diff options
| author | pinky <> | 2007-01-07 21:41:39 +0000 |
|---|---|---|
| committer | pinky <> | 2007-01-07 21:41:39 +0000 |
| commit | 05bb5d58295cd959c34f1fbcd5b605cd6dafaacc (patch) | |
| tree | 3b3651037037c1617a54ef3d216e8c11ea8cdddf /bin/reportlab/graphics/charts | |
| parent | 62524af2f41454a2302efb21f15a95318d257a76 (diff) | |
Repotrlab 2.0
bzr revid: pinky-b8333ccaab5dad8ff965b6c380fadadf30d5212a
Diffstat (limited to 'bin/reportlab/graphics/charts')
| -rw-r--r-- | bin/reportlab/graphics/charts/__init__.py | 4 | ||||
| -rw-r--r-- | bin/reportlab/graphics/charts/areas.py | 92 | ||||
| -rw-r--r-- | bin/reportlab/graphics/charts/axes.py | 1979 | ||||
| -rw-r--r-- | bin/reportlab/graphics/charts/barcharts.py | 1972 | ||||
| -rw-r--r-- | bin/reportlab/graphics/charts/dotbox.py | 165 | ||||
| -rw-r--r-- | bin/reportlab/graphics/charts/doughnut.py | 349 | ||||
| -rw-r--r-- | bin/reportlab/graphics/charts/legends.py | 611 | ||||
| -rw-r--r-- | bin/reportlab/graphics/charts/linecharts.py | 695 | ||||
| -rw-r--r-- | bin/reportlab/graphics/charts/lineplots.py | 1083 | ||||
| -rw-r--r-- | bin/reportlab/graphics/charts/markers.py | 81 | ||||
| -rw-r--r-- | bin/reportlab/graphics/charts/piecharts.py | 1279 | ||||
| -rw-r--r-- | bin/reportlab/graphics/charts/slidebox.py | 186 | ||||
| -rw-r--r-- | bin/reportlab/graphics/charts/spider.py | 407 | ||||
| -rw-r--r-- | bin/reportlab/graphics/charts/textlabels.py | 442 | ||||
| -rw-r--r-- | bin/reportlab/graphics/charts/utils.py | 191 | ||||
| -rw-r--r-- | bin/reportlab/graphics/charts/utils3d.py | 233 |
16 files changed, 9769 insertions, 0 deletions
diff --git a/bin/reportlab/graphics/charts/__init__.py b/bin/reportlab/graphics/charts/__init__.py new file mode 100644 index 00000000000..6b2a42748cc --- /dev/null +++ b/bin/reportlab/graphics/charts/__init__.py @@ -0,0 +1,4 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/__init__.py +__version__=''' $Id: __init__.py 2385 2004-06-17 15:26:05Z rgbecker $ '''
\ No newline at end of file diff --git a/bin/reportlab/graphics/charts/areas.py b/bin/reportlab/graphics/charts/areas.py new file mode 100644 index 00000000000..7588d924009 --- /dev/null +++ b/bin/reportlab/graphics/charts/areas.py @@ -0,0 +1,92 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/areas.py +"""This module defines a Area mixin classes +""" +__version__=''' $Id: areas.py 2385 2004-06-17 15:26:05Z rgbecker $ ''' +from reportlab.lib.validators import isNumber, isColor, isColorOrNone, isNoneOrShape +from reportlab.graphics.widgetbase import Widget +from reportlab.graphics.shapes import Rect, Group, Line, Polygon +from reportlab.lib.attrmap import AttrMap, AttrMapValue + +class PlotArea(Widget): + "Abstract base class representing a chart's plot area, pretty unusable by itself." + _attrMap = AttrMap( + x = AttrMapValue(isNumber, desc='X position of the lower-left corner of the chart.'), + y = AttrMapValue(isNumber, desc='Y position of the lower-left corner of the chart.'), + width = AttrMapValue(isNumber, desc='Width of the chart.'), + height = AttrMapValue(isNumber, desc='Height of the chart.'), + strokeColor = AttrMapValue(isColorOrNone, desc='Color of the plot area border.'), + strokeWidth = AttrMapValue(isNumber, desc='Width plot area border.'), + fillColor = AttrMapValue(isColorOrNone, desc='Color of the plot area interior.'), + background = AttrMapValue(isNoneOrShape, desc='Handle to background object.'), + debug = AttrMapValue(isNumber, desc='Used only for debugging.'), + ) + + def __init__(self): + self.x = 20 + self.y = 10 + self.height = 85 + self.width = 180 + self.strokeColor = None + self.strokeWidth = 1 + self.fillColor = None + self.background = None + self.debug = 0 + + def makeBackground(self): + if self.background is not None: + BG = self.background + if isinstance(BG,Group): + g = BG + for bg in g.contents: + bg.x = self.x + bg.y = self.y + bg.width = self.width + bg.height = self.height + else: + g = Group() + if type(BG) not in (type(()),type([])): BG=(BG,) + for bg in BG: + bg.x = self.x + bg.y = self.y + bg.width = self.width + bg.height = self.height + g.add(bg) + return g + else: + strokeColor,strokeWidth,fillColor=self.strokeColor, self.strokeWidth, self.fillColor + if (strokeWidth and strokeColor) or fillColor: + g = Group() + _3d_dy = getattr(self,'_3d_dy',None) + x = self.x + y = self.y + h = self.height + w = self.width + if _3d_dy is not None: + _3d_dx = self._3d_dx + if fillColor and not strokeColor: + from reportlab.lib.colors import Blacker + c = Blacker(fillColor, getattr(self,'_3d_blacken',0.7)) + else: + c = strokeColor + if not strokeWidth: strokeWidth = 0.5 + if fillColor or strokeColor or c: + bg = Polygon([x,y,x,y+h,x+_3d_dx,y+h+_3d_dy,x+w+_3d_dx,y+h+_3d_dy,x+w+_3d_dx,y+_3d_dy,x+w,y], + strokeColor=strokeColor or c or grey, strokeWidth=strokeWidth, fillColor=fillColor) + g.add(bg) + g.add(Line(x,y,x+_3d_dx,y+_3d_dy, strokeWidth=0.5, strokeColor=c)) + g.add(Line(x+_3d_dx,y+_3d_dy, x+_3d_dx,y+h+_3d_dy,strokeWidth=0.5, strokeColor=c)) + fc = Blacker(c, getattr(self,'_3d_blacken',0.8)) + g.add(Polygon([x,y,x+_3d_dx,y+_3d_dy,x+w+_3d_dx,y+_3d_dy,x+w,y], + strokeColor=strokeColor or c or grey, strokeWidth=strokeWidth, fillColor=fc)) + bg = Line(x+_3d_dx,y+_3d_dy, x+w+_3d_dx,y+_3d_dy,strokeWidth=0.5, strokeColor=c) + else: + bg = None + else: + bg = Rect(x, y, w, h, + strokeColor=strokeColor, strokeWidth=strokeWidth, fillColor=fillColor) + if bg: g.add(bg) + return g + else: + return None diff --git a/bin/reportlab/graphics/charts/axes.py b/bin/reportlab/graphics/charts/axes.py new file mode 100644 index 00000000000..ab0be2ad060 --- /dev/null +++ b/bin/reportlab/graphics/charts/axes.py @@ -0,0 +1,1979 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/axes.py +"""Collection of axes for charts. + +The current collection comprises axes for charts using cartesian +coordinate systems. All axes might have tick marks and labels. +There are two dichotomies for axes: one of X and Y flavours and +another of category and value flavours. + +Category axes have an ordering but no metric. They are divided +into a number of equal-sized buckets. Their tick marks or labels, +if available, go BETWEEN the buckets, and the labels are placed +below to/left of the X/Y-axis, respectively. + + Value axes have an ordering AND metric. They correspond to a nu- + meric quantity. Value axis have a real number quantity associated + with it. The chart tells it where to go. + The most basic axis divides the number line into equal spaces + and has tickmarks and labels associated with each; later we + will add variants where you can specify the sampling + interval. + +The charts using axis tell them where the labels should be placed. + +Axes of complementary X/Y flavours can be connected to each other +in various ways, i.e. with a specific reference point, like an +x/value axis to a y/value (or category) axis. In this case the +connection can be either at the top or bottom of the former or +at any absolute value (specified in points) or at some value of +the former axes in its own coordinate system. +""" +__version__=''' $Id: axes.py 2838 2006-04-18 17:47:54Z rgbecker $ ''' + +import string +from types import FunctionType, StringType, TupleType, ListType + +from reportlab.lib.validators import isNumber, isNumberOrNone, isListOfStringsOrNone, isListOfNumbers, \ + isListOfNumbersOrNone, isColorOrNone, OneOf, isBoolean, SequenceOf, \ + isString, EitherOr +from reportlab.lib.attrmap import * +from reportlab.lib import normalDate +from reportlab.graphics.shapes import Drawing, Line, PolyLine, Group, STATE_DEFAULTS, _textBoxLimits, _rotatedBoxLimits +from reportlab.graphics.widgetbase import Widget, TypedPropertyCollection +from reportlab.graphics.charts.textlabels import Label +from reportlab.graphics.charts.utils import nextRoundNumber + + +# Helpers. + +def _findMinMaxValue(V, x, default, func, special=None): + if type(V[0][0]) in (TupleType,ListType): + if special: + f=lambda T,x=x,special=special,func=func: special(T,x,func) + else: + f=lambda T,x=x: T[x] + V=map(lambda e,f=f: map(f,e),V) + V = filter(len,map(lambda x: filter(lambda x: x is not None,x),V)) + if len(V)==0: return default + return func(map(func,V)) + +def _findMin(V, x, default,special=None): + '''find minimum over V[i][x]''' + return _findMinMaxValue(V,x,default,min,special=special) + +def _findMax(V, x, default,special=None): + '''find maximum over V[i][x]''' + return _findMinMaxValue(V,x,default,max,special=special) + +def _allInt(values): + '''true if all values are int''' + for v in values: + try: + if int(v)!=v: return 0 + except: + return 0 + return 1 + +class _AxisG(Widget): + def _get_line_pos(self,v): + v = self.scale(v) + try: + v = v[0] + except: + pass + return v + + def _cxLine(self,x,start,end): + x = self._get_line_pos(x) + return Line(x, self._y + start, x, self._y + end) + + def _cyLine(self,y,start,end): + y = self._get_line_pos(y) + return Line(self._x + start, y, self._x + end, y) + + def _cxLine3d(self,x,start,end,_3d_dx,_3d_dy): + x = self._get_line_pos(x) + y0 = self._y + start + y1 = self._y + end + y0, y1 = min(y0,y1),max(y0,y1) + x1 = x + _3d_dx + return PolyLine([x,y0,x1,y0+_3d_dy,x1,y1+_3d_dy],strokeLineJoin=1) + + def _cyLine3d(self,y,start,end,_3d_dx,_3d_dy): + y = self._get_line_pos(y) + x0 = self._x + start + x1 = self._x + end + x0, x1 = min(x0,x1),max(x0,x1) + y1 = y + _3d_dy + return PolyLine([x0,y,x0+_3d_dx,y1,x1+_3d_dx,y1],strokeLineJoin=1) + + def _getLineFunc(self, start, end, parent=None): + _3d_dx = getattr(parent,'_3d_dx',None) + if _3d_dx is not None: + _3d_dy = getattr(parent,'_3d_dy',None) + f = self._dataIndex and self._cyLine3d or self._cxLine3d + return lambda v, s=start, e=end, f=f,_3d_dx=_3d_dx,_3d_dy=_3d_dy: f(v,s,e,_3d_dx=_3d_dx,_3d_dy=_3d_dy) + else: + f = self._dataIndex and self._cyLine or self._cxLine + return lambda v, s=start, e=end, f=f: f(v,s,e) + + def _makeLines(self,g,start,end,strokeColor,strokeWidth,strokeDashArray,parent=None): + func = self._getLineFunc(start,end,parent) + for t in self._tickValues: + L = func(t) + L.strokeColor = strokeColor + L.strokeWidth = strokeWidth + L.strokeDashArray = strokeDashArray + g.add(L) + + def makeGrid(self,g,dim=None,parent=None): + '''this is only called by a container object''' + s = self.gridStart + e = self.gridEnd + if dim: + s = s is None and dim[0] + e = e is None and dim[0]+dim[1] + c = self.gridStrokeColor + if self.visibleGrid and (s or e) and c is not None: + if self._dataIndex: offs = self._x + else: offs = self._y + self._makeLines(g,s-offs,e-offs,c,self.gridStrokeWidth,self.gridStrokeDashArray,parent=parent) + +# Category axes. +class CategoryAxis(_AxisG): + "Abstract category axis, unusable in itself." + _nodoc = 1 + _attrMap = AttrMap( + visible = AttrMapValue(isBoolean, desc='Display entire object, if true.'), + visibleAxis = AttrMapValue(isBoolean, desc='Display axis line, if true.'), + visibleTicks = AttrMapValue(isBoolean, desc='Display axis ticks, if true.'), + visibleLabels = AttrMapValue(isBoolean, desc='Display axis labels, if true.'), + visibleGrid = AttrMapValue(isBoolean, desc='Display axis grid, if true.'), + strokeWidth = AttrMapValue(isNumber, desc='Width of axis line and ticks.'), + strokeColor = AttrMapValue(isColorOrNone, desc='Color of axis line and ticks.'), + strokeDashArray = AttrMapValue(isListOfNumbersOrNone, desc='Dash array used for axis line.'), + gridStrokeWidth = AttrMapValue(isNumber, desc='Width of grid lines.'), + gridStrokeColor = AttrMapValue(isColorOrNone, desc='Color of grid lines.'), + gridStrokeDashArray = AttrMapValue(isListOfNumbersOrNone, desc='Dash array used for grid lines.'), + gridStart = AttrMapValue(isNumberOrNone, desc='Start of grid lines wrt axis origin'), + gridEnd = AttrMapValue(isNumberOrNone, desc='End of grid lines wrt axis origin'), + labels = AttrMapValue(None, desc='Handle of the axis labels.'), + categoryNames = AttrMapValue(isListOfStringsOrNone, desc='List of category names.'), + joinAxis = AttrMapValue(None, desc='Join both axes if true.'), + joinAxisPos = AttrMapValue(isNumberOrNone, desc='Position at which to join with other axis.'), + reverseDirection = AttrMapValue(isBoolean, desc='If true reverse category direction.'), + style = AttrMapValue(OneOf('parallel','stacked','parallel_3d'),"How common category bars are plotted"), + labelAxisMode = AttrMapValue(OneOf('high','low','axis'), desc="Like joinAxisMode, but for the axis labels"), + tickShift = AttrMapValue(isBoolean, desc='Tick shift typically'), + ) + + def __init__(self): + assert self.__class__.__name__!='CategoryAxis', "Abstract Class CategoryAxis Instantiated" + # private properties set by methods. The initial values + # here are to make demos easy; they would always be + # overridden in real life. + self._x = 50 + self._y = 50 + self._length = 100 + self._catCount = 0 + + # public properties + self.visible = 1 + self.visibleAxis = 1 + self.visibleTicks = 1 + self.visibleLabels = 1 + self.visibleGrid = 0 + + self.strokeWidth = 1 + self.strokeColor = STATE_DEFAULTS['strokeColor'] + self.strokeDashArray = STATE_DEFAULTS['strokeDashArray'] + self.gridStrokeWidth = 0.25 + self.gridStrokeColor = STATE_DEFAULTS['strokeColor'] + self.gridStrokeDashArray = STATE_DEFAULTS['strokeDashArray'] + self.gridStart = self.gridEnd = 0 + self.labels = TypedPropertyCollection(Label) + # if None, they don't get labels. If provided, + # you need one name per data point and they are + # used for label text. + self.categoryNames = None + self.joinAxis = None + self.joinAxisPos = None + self.joinAxisMode = None + self.labelAxisMode = 'axis' + self.reverseDirection = 0 + self.style = 'parallel' + + #various private things which need to be initialized + self._labelTextFormat = None + self.tickShift = 0 + + def setPosition(self, x, y, length): + # ensure floating point + self._x = x + self._y = y + self._length = length + + + def configure(self, multiSeries,barWidth=None): + self._catCount = max(map(len,multiSeries)) + self._barWidth = barWidth or (self._length/float(self._catCount or 1)) + self._calcTickmarkPositions() + + def _calcTickmarkPositions(self): + n = self._catCount + if self.tickShift: + self._tickValues = [t+0.5 for t in xrange(n)] + else: + self._tickValues = range(n+1) + + def draw(self): + g = Group() + + if not self.visible: + return g + + g.add(self.makeAxis()) + g.add(self.makeTicks()) + g.add(self.makeTickLabels()) + + return g + + def _scale(self,idx): + if self.reverseDirection: idx = self._catCount-idx-1 + return idx + +def _assertYAxis(axis): + acn = axis.__class__.__name__ + assert acn[0]=='Y' or acn[:4]=='AdjY', "Cannot connect to other axes (%s), but Y- ones." % acn +def _assertXAxis(axis): + acn = axis.__class__.__name__ + assert acn[0]=='X' or acn[:11]=='NormalDateX', "Cannot connect to other axes (%s), but X- ones." % acn + +class XCategoryAxis(CategoryAxis): + "X/category axis" + + _attrMap = AttrMap(BASE=CategoryAxis, + tickUp = AttrMapValue(isNumber, + desc='Tick length up the axis.'), + tickDown = AttrMapValue(isNumber, + desc='Tick length down the axis.'), + joinAxisMode = AttrMapValue(OneOf('bottom', 'top', 'value', 'points', None), + desc="Mode used for connecting axis ('bottom', 'top', 'value', 'points', None)."), + ) + + _dataIndex = 0 + + def __init__(self): + CategoryAxis.__init__(self) + self.labels.boxAnchor = 'n' #north - top edge + self.labels.dy = -5 + # ultra-simple tick marks for now go between categories + # and have same line style as axis - need more + self.tickUp = 0 # how far into chart does tick go? + self.tickDown = 5 # how far below axis does tick go? + + + def demo(self): + self.setPosition(30, 70, 140) + self.configure([(10,20,30,40,50)]) + + self.categoryNames = ['One','Two','Three','Four','Five'] + # all labels top-centre aligned apart from the last + self.labels.boxAnchor = 'n' + self.labels[4].boxAnchor = 'e' + self.labels[4].angle = 90 + + d = Drawing(200, 100) + d.add(self) + return d + + + def joinToAxis(self, yAxis, mode='bottom', pos=None): + "Join with y-axis using some mode." + + _assertYAxis(yAxis) + if mode == 'bottom': + self._x = yAxis._x + self._y = yAxis._y + elif mode == 'top': + self._x = yAxis._x + self._y = yAxis._y + yAxis._length + elif mode == 'value': + self._x = yAxis._x + self._y = yAxis.scale(pos) + elif mode == 'points': + self._x = yAxis._x + self._y = pos + + def _joinToAxis(self): + ja = self.joinAxis + if ja: + jam = self.joinAxisMode + jap = self.joinAxisPos + jta = self.joinToAxis + if jam in ('bottom', 'top'): + jta(ja, mode=jam) + elif jam in ('value', 'points'): + jta(ja, mode=jam, pos=jap) + + def scale(self, idx): + """returns the x position and width in drawing units of the slice""" + return (self._x + self._scale(idx)*self._barWidth, self._barWidth) + + def makeAxis(self): + g = Group() + self._joinToAxis() + if not self.visibleAxis: return g + + axis = Line(self._x, self._y, self._x + self._length, self._y) + axis.strokeColor = self.strokeColor + axis.strokeWidth = self.strokeWidth + axis.strokeDashArray = self.strokeDashArray + g.add(axis) + + return g + + def makeTicks(self): + g = Group() + if not self.visibleTicks: return g + if self.tickUp or self.tickDown: + self._makeLines(g,self.tickUp,-self.tickDown,self.strokeColor,self.strokeWidth,self.strokeDashArray) + return g + + def _labelAxisPos(self): + axis = self.joinAxis + if axis: + mode = self.labelAxisMode + if mode == 'low': + return axis._y + elif mode == 'high': + return axis._y + axis._length + return self._y + + def makeTickLabels(self): + g = Group() + + if not self.visibleLabels: return g + + categoryNames = self.categoryNames + if categoryNames is not None: + catCount = self._catCount + n = len(categoryNames) + reverseDirection = self.reverseDirection + barWidth = self._barWidth + _y = self._labelAxisPos() + _x = self._x + + for i in xrange(catCount): + if reverseDirection: ic = catCount-i-1 + else: ic = i + if ic>=n: continue + x = _x + (i+0.5) * barWidth + label = self.labels[i] + label.setOrigin(x, _y) + label.setText(categoryNames[ic] or '') + g.add(label) + + return g + + +class YCategoryAxis(CategoryAxis): + "Y/category axis" + + _attrMap = AttrMap(BASE=CategoryAxis, + tickLeft = AttrMapValue(isNumber, + desc='Tick length left of the axis.'), + tickRight = AttrMapValue(isNumber, + desc='Tick length right of the axis.'), + joinAxisMode = AttrMapValue(OneOf(('left', 'right', 'value', 'points', None)), + desc="Mode used for connecting axis ('left', 'right', 'value', 'points', None)."), + ) + + _dataIndex = 1 + + + def __init__(self): + CategoryAxis.__init__(self) + self.labels.boxAnchor = 'e' #east - right edge + self.labels.dx = -5 + # ultra-simple tick marks for now go between categories + # and have same line style as axis - need more + self.tickLeft = 5 # how far left of axis does tick go? + self.tickRight = 0 # how far right of axis does tick go? + + + def demo(self): + self.setPosition(50, 10, 80) + self.configure([(10,20,30)]) + self.categoryNames = ['One','Two','Three'] + # all labels top-centre aligned apart from the last + self.labels.boxAnchor = 'e' + self.labels[2].boxAnchor = 's' + self.labels[2].angle = 90 + + d = Drawing(200, 100) + d.add(self) + return d + + + def joinToAxis(self, xAxis, mode='left', pos=None): + "Join with x-axis using some mode." + + _assertXAxis(xAxis) + + if mode == 'left': + self._x = xAxis._x * 1.0 + self._y = xAxis._y * 1.0 + elif mode == 'right': + self._x = (xAxis._x + xAxis._length) * 1.0 + self._y = xAxis._y * 1.0 + elif mode == 'value': + self._x = xAxis.scale(pos) * 1.0 + self._y = xAxis._y * 1.0 + elif mode == 'points': + self._x = pos * 1.0 + self._y = xAxis._y * 1.0 + + def _joinToAxis(self): + ja = self.joinAxis + if ja: + jam = self.joinAxisMode + jap = self.joinAxisPos + jta = self.joinToAxis + if jam in ('left', 'right'): + jta(ja, mode=jam) + elif jam in ('value', 'points'): + jta(ja, mode=jam, pos=jap) + + def scale(self, idx): + "Returns the y position and width in drawing units of the slice." + return (self._y + self._scale(idx)*self._barWidth, self._barWidth) + + def makeAxis(self): + g = Group() + self._joinToAxis() + if not self.visibleAxis: return g + + axis = Line(self._x, self._y, self._x, self._y + self._length) + axis.strokeColor = self.strokeColor + axis.strokeWidth = self.strokeWidth + axis.strokeDashArray = self.strokeDashArray + g.add(axis) + + return g + + def makeTicks(self): + g = Group() + if not self.visibleTicks: return g + if self.tickLeft or self.tickRight: + self._makeLines(g,-self.tickLeft,self.tickRight,self.strokeColor,self.strokeWidth,self.strokeDashArray) + return g + + def _labelAxisPos(self): + axis = self.joinAxis + if axis: + mode = self.labelAxisMode + if mode == 'low': + return axis._x + elif mode == 'high': + return axis._x + axis._length + return self._x + + def makeTickLabels(self): + g = Group() + + if not self.visibleTicks: return g + + categoryNames = self.categoryNames + if categoryNames is not None: + catCount = self._catCount + n = len(categoryNames) + reverseDirection = self.reverseDirection + barWidth = self._barWidth + labels = self.labels + _x = self._labelAxisPos() + _y = self._y + for i in xrange(catCount): + if reverseDirection: ic = catCount-i-1 + else: ic = i + if ic>=n: continue + y = _y + (i+0.5) * barWidth + label = labels[i] + label.setOrigin(_x, y) + label.setText(categoryNames[ic] or '') + g.add(label) + return g + + +# Value axes. +class ValueAxis(_AxisG): + "Abstract value axis, unusable in itself." + + _attrMap = AttrMap( + forceZero = AttrMapValue(EitherOr((isBoolean,OneOf('near'))), desc='Ensure zero in range if true.'), + visible = AttrMapValue(isBoolean, desc='Display entire object, if true.'), + visibleAxis = AttrMapValue(isBoolean, desc='Display axis line, if true.'), + visibleLabels = AttrMapValue(isBoolean, desc='Display axis labels, if true.'), + visibleTicks = AttrMapValue(isBoolean, desc='Display axis ticks, if true.'), + visibleGrid = AttrMapValue(isBoolean, desc='Display axis grid, if true.'), + strokeWidth = AttrMapValue(isNumber, desc='Width of axis line and ticks.'), + strokeColor = AttrMapValue(isColorOrNone, desc='Color of axis line and ticks.'), + strokeDashArray = AttrMapValue(isListOfNumbersOrNone, desc='Dash array used for axis line.'), + gridStrokeWidth = AttrMapValue(isNumber, desc='Width of grid lines.'), + gridStrokeColor = AttrMapValue(isColorOrNone, desc='Color of grid lines.'), + gridStrokeDashArray = AttrMapValue(isListOfNumbersOrNone, desc='Dash array used for grid lines.'), + gridStart = AttrMapValue(isNumberOrNone, desc='Start of grid lines wrt axis origin'), + gridEnd = AttrMapValue(isNumberOrNone, desc='End of grid lines wrt axis origin'), + minimumTickSpacing = AttrMapValue(isNumber, desc='Minimum value for distance between ticks.'), + maximumTicks = AttrMapValue(isNumber, desc='Maximum number of ticks.'), + labels = AttrMapValue(None, desc='Handle of the axis labels.'), + labelTextFormat = AttrMapValue(None, desc='Formatting string or function used for axis labels.'), + labelTextPostFormat = AttrMapValue(None, desc='Extra Formatting string.'), + labelTextScale = AttrMapValue(isNumberOrNone, desc='Scaling for label tick values.'), + valueMin = AttrMapValue(isNumberOrNone, desc='Minimum value on axis.'), + valueMax = AttrMapValue(isNumberOrNone, desc='Maximum value on axis.'), + valueStep = AttrMapValue(isNumberOrNone, desc='Step size used between ticks.'), + valueSteps = AttrMapValue(isListOfNumbersOrNone, desc='List of step sizes used between ticks.'), + avoidBoundFrac = AttrMapValue(EitherOr((isNumberOrNone,SequenceOf(isNumber,emptyOK=0,lo=2,hi=2))), desc='Fraction of interval to allow above and below.'), + rangeRound=AttrMapValue(OneOf('none','both','ceiling','floor'),'How to round the axis limits'), + zrangePref = AttrMapValue(isNumberOrNone, desc='Zero range axis limit preference.'), + style = AttrMapValue(OneOf('normal','stacked','parallel_3d'),"How values are plotted!"), + ) + + def __init__(self): + assert self.__class__.__name__!='ValueAxis', 'Abstract Class ValueAxis Instantiated' + self._configured = 0 + # private properties set by methods. The initial values + # here are to make demos easy; they would always be + # overridden in real life. + self._x = 50 + self._y = 50 + self._length = 100 + + # public properties + self.visible = 1 + self.visibleAxis = 1 + self.visibleLabels = 1 + self.visibleTicks = 1 + self.visibleGrid = 0 + self.forceZero = 0 + + self.strokeWidth = 1 + self.strokeColor = STATE_DEFAULTS['strokeColor'] + self.strokeDashArray = STATE_DEFAULTS['strokeDashArray'] + self.gridStrokeWidth = 0.25 + self.gridStrokeColor = STATE_DEFAULTS['strokeColor'] + self.gridStrokeDashArray = STATE_DEFAULTS['strokeDashArray'] + self.gridStart = self.gridEnd = 0 + + self.labels = TypedPropertyCollection(Label) + self.labels.angle = 0 + + # how close can the ticks be? + self.minimumTickSpacing = 10 + self.maximumTicks = 7 + + # a format string like '%0.2f' + # or a function which takes the value as an argument and returns a string + self._labelTextFormat = self.labelTextFormat = self.labelTextPostFormat = self.labelTextScale = None + + # if set to None, these will be worked out for you. + # if you override any or all of them, your values + # will be used. + self.valueMin = None + self.valueMax = None + self.valueStep = None + self.avoidBoundFrac = None + self.rangeRound = 'none' + self.zrangePref = 0 + self.style = 'normal' + + def setPosition(self, x, y, length): + # ensure floating point + self._x = x * 1.0 + self._y = y * 1.0 + self._length = length * 1.0 + + def configure(self, dataSeries): + """Let the axis configure its scale and range based on the data. + + Called after setPosition. Let it look at a list of lists of + numbers determine the tick mark intervals. If valueMin, + valueMax and valueStep are configured then it + will use them; if any of them are set to None it + will look at the data and make some sensible decision. + You may override this to build custom axes with + irregular intervals. It creates an internal + variable self._values, which is a list of numbers + to use in plotting. + """ + self._setRange(dataSeries) + self._calcTickmarkPositions() + self._calcScaleFactor() + self._configured = 1 + + def _getValueStepAndTicks(self, valueMin, valueMax,cache={}): + try: + K = (valueMin,valueMax) + r = cache[K] + except: + self._valueMin = valueMin + self._valueMax = valueMax + T = self._calcTickPositions() + if len(T)>1: + valueStep = T[1]-T[0] + else: + oVS = self.valueStep + self.valueStep = None + T = self._calcTickPositions() + self.valueStep = oVS + if len(T)>1: + valueStep = T[1]-T[0] + else: + valueStep = self._valueStep + r = cache[K] = valueStep, T, valueStep*1e-8 + return r + + def _setRange(self, dataSeries): + """Set minimum and maximum axis values. + + The dataSeries argument is assumed to be a list of data + vectors. Each vector is itself a list or tuple of numbers. + + Returns a min, max tuple. + """ + + oMin = valueMin = self.valueMin + oMax = valueMax = self.valueMax + rangeRound = self.rangeRound + if valueMin is None: valueMin = self._cValueMin = _findMin(dataSeries,self._dataIndex,0) + if valueMax is None: valueMax = self._cValueMax = _findMax(dataSeries,self._dataIndex,0) + if valueMin == valueMax: + if valueMax==0: + if oMin is None and oMax is None: + zrp = getattr(self,'zrangePref',0) + if zrp>0: + valueMax = zrp + valueMin = 0 + elif zrp<0: + valueMax = 0 + valueMin = zrp + else: + valueMax = 0.01 + valueMin = -0.01 + elif self.valueMin is None: + valueMin = -0.01 + else: + valueMax = 0.01 + else: + if valueMax>0: + valueMax = 1.2*valueMax + valueMin = 0.0 + else: + valueMax = 0.0 + valueMin = 1.2*valueMin + + if getattr(self,'_bubblePlot',None): + bubbleMax = float(_findMax(dataSeries,2,0)) + frac=.25 + bubbleV=frac*(valueMax-valueMin) + self._bubbleV = bubbleV + self._bubbleMax = bubbleMax + self._bubbleRadius = frac*self._length + def special(T,x,func,bubbleV=bubbleV,bubbleMax=bubbleMax): + try: + v = T[2] + except IndexError: + v = bubbleMAx*0.1 + bubbleV *= (v/bubbleMax)**0.5 + return func(T[x]+bubbleV,T[x]-bubbleV) + if oMin is None: valueMin = self._cValueMin = _findMin(dataSeries,self._dataIndex,0,special=special) + if oMax is None: valueMax = self._cValueMax = _findMax(dataSeries,self._dataIndex,0,special=special) + + forceZero = self.forceZero + if forceZero: + if forceZero=='near': + forceZero = min(abs(valueMin),abs(valueMax)) <= 5*(valueMax-valueMin) + if forceZero: + if valueMax<0: valueMax=0 + elif valueMin>0: valueMin = 0 + + abf = self.avoidBoundFrac + do_rr = not getattr(self,'valueSteps',None) + do_abf = abf and do_rr + if type(abf) not in (TupleType,ListType): + abf = abf, abf + do_rr = rangeRound is not 'none' and do_rr + if do_rr: + rrn = rangeRound in ['both','floor'] + rrx = rangeRound in ['both','ceiling'] + else: + rrn = rrx = 0 + + go = do_rr or do_abf + cache = {} + cMin = valueMin + cMax = valueMax + iter = 0 + while go and iter<=10: + iter += 1 + go = 0 + if do_abf: + valueStep, T, fuzz = self._getValueStepAndTicks(valueMin, valueMax, cache) + i0 = valueStep*abf[0] + i1 = valueStep*abf[1] + if rrn: v = T[0] + else: v = valueMin + u = cMin-i0 + if abs(v)>fuzz and v>=u+fuzz: + valueMin = u + go = 1 + if rrx: v = T[-1] + else: v = valueMax + u = cMax+i1 + if abs(v)>fuzz and v<=u-fuzz: + valueMax = u + go = 1 + + if do_rr: + valueStep, T, fuzz = self._getValueStepAndTicks(valueMin, valueMax, cache) + if rrn: + if valueMin<T[0]-fuzz: + valueMin = T[0]-valueStep + go = 1 + else: + go = valueMin>=T[0]+fuzz + valueMin = T[0] + if rrx: + if valueMax>T[-1]+fuzz: + valueMax = T[-1]+valueStep + go = 1 + else: + go = valueMax<=T[-1]-fuzz + valueMax = T[-1] + + self._valueMin, self._valueMax = valueMin, valueMax + self._rangeAdjust() + + def _rangeAdjust(self): + """Override this if you want to alter the calculated range. + + E.g. if want a minumamum range of 30% or don't want 100% + as the first point. + """ + pass + + def _adjustAxisTicks(self): + '''Override if you want to put slack at the ends of the axis + eg if you don't want the last tick to be at the bottom etc + ''' + pass + + def _calcScaleFactor(self): + """Calculate the axis' scale factor. + This should be called only *after* the axis' range is set. + Returns a number. + """ + self._scaleFactor = self._length / float(self._valueMax - self._valueMin) + return self._scaleFactor + + def _calcTickPositions(self): + self._calcValueStep() + valueMin, valueMax, valueStep = self._valueMin, self._valueMax, self._valueStep + fuzz = 1e-8*valueStep + rangeRound = self.rangeRound + i0 = int(float(valueMin)/valueStep) + v = i0*valueStep + if rangeRound in ('both','floor'): + if v>valueMin+fuzz: i0 -= 1 + elif v<valueMin-fuzz: i0 += 1 + i1 = int(float(valueMax)/valueStep) + v = i1*valueStep + if rangeRound in ('both','ceiling'): + if v<valueMax-fuzz: i1 += 1 + elif v>valueMax+fuzz: i1 -= 1 + return [i*valueStep for i in xrange(i0,i1+1)] + + def _calcTickmarkPositions(self): + """Calculate a list of tick positions on the axis. Returns a list of numbers.""" + self._tickValues = getattr(self,'valueSteps',None) + if self._tickValues: return self._tickValues + self._tickValues = self._calcTickPositions() + self._adjustAxisTicks() + return self._tickValues + + def _calcValueStep(self): + '''Calculate _valueStep for the axis or get from valueStep.''' + if self.valueStep is None: + rawRange = self._valueMax - self._valueMin + rawInterval = rawRange / min(float(self.maximumTicks-1),(float(self._length)/self.minimumTickSpacing)) + self._valueStep = nextRoundNumber(rawInterval) + else: + self._valueStep = self.valueStep + + def _allIntTicks(self): + return _allInt(self._tickValues) + + def makeTickLabels(self): + g = Group() + if not self.visibleLabels: return g + + f = self._labelTextFormat # perhaps someone already set it + if f is None: + f = self.labelTextFormat or (self._allIntTicks() and '%.0f' or str) + elif f is str and self._allIntTicks(): f = '%.0f' + post = self.labelTextPostFormat + scl = self.labelTextScale + pos = [self._x, self._y] + d = self._dataIndex + labels = self.labels + + i = 0 + for tick in self._tickValues: + if f and labels[i].visible: + v = self.scale(tick) + if scl is not None: + t = tick*scl + else: + t = tick + if type(f) is StringType: txt = f % t + elif type(f) in (TupleType,ListType): + #it's a list, use as many items as we get + if i < len(f): + txt = f[i] + else: + txt = '' + elif callable(f): + txt = f(t) + else: + raise ValueError, 'Invalid labelTextFormat %s' % f + if post: txt = post % txt + label = labels[i] + pos[d] = v + apply(label.setOrigin,pos) + label.setText(txt) + g.add(label) + i = i + 1 + + return g + + def draw(self): + g = Group() + + if not self.visible: + return g + + g.add(self.makeAxis()) + g.add(self.makeTicks()) + g.add(self.makeTickLabels()) + + return g + + +class XValueAxis(ValueAxis): + "X/value axis" + + _attrMap = AttrMap(BASE=ValueAxis, + tickUp = AttrMapValue(isNumber, + desc='Tick length up the axis.'), + tickDown = AttrMapValue(isNumber, + desc='Tick length down the axis.'), + joinAxis = AttrMapValue(None, + desc='Join both axes if true.'), + joinAxisMode = AttrMapValue(OneOf('bottom', 'top', 'value', 'points', None), + desc="Mode used for connecting axis ('bottom', 'top', 'value', 'points', None)."), + joinAxisPos = AttrMapValue(isNumberOrNone, + desc='Position at which to join with other axis.'), + ) + + # Indicate the dimension of the data we're interested in. + _dataIndex = 0 + + def __init__(self): + ValueAxis.__init__(self) + + self.labels.boxAnchor = 'n' + self.labels.dx = 0 + self.labels.dy = -5 + + self.tickUp = 0 + self.tickDown = 5 + + self.joinAxis = None + self.joinAxisMode = None + self.joinAxisPos = None + + + def demo(self): + self.setPosition(20, 50, 150) + self.configure([(10,20,30,40,50)]) + + d = Drawing(200, 100) + d.add(self) + return d + + + def joinToAxis(self, yAxis, mode='bottom', pos=None): + "Join with y-axis using some mode." + _assertYAxis(yAxis) + if mode == 'bottom': + self._x = yAxis._x * 1.0 + self._y = yAxis._y * 1.0 + elif mode == 'top': + self._x = yAxis._x * 1.0 + self._y = (yAxis._y + yAxis._length) * 1.0 + elif mode == 'value': + self._x = yAxis._x * 1.0 + self._y = yAxis.scale(pos) * 1.0 + elif mode == 'points': + self._x = yAxis._x * 1.0 + self._y = pos * 1.0 + + def _joinToAxis(self): + ja = self.joinAxis + if ja: + jam = self.joinAxisMode + jap = self.joinAxisPos + jta = self.joinToAxis + if jam in ('bottom', 'top'): + jta(ja, mode=jam) + elif jam in ('value', 'points'): + jta(ja, mode=jam, pos=jap) + + def scale(self, value): + """Converts a numeric value to a Y position. + + The chart first configures the axis, then asks it to + work out the x value for each point when plotting + lines or bars. You could override this to do + logarithmic axes. + """ + + msg = "Axis cannot scale numbers before it is configured" + assert self._configured, msg + if value is None: + value = 0 + return self._x + self._scaleFactor * (value - self._valueMin) + + + def makeAxis(self): + g = Group() + self._joinToAxis() + if not self.visibleAxis: return g + + axis = Line(self._x, self._y, self._x + self._length, self._y) + axis.strokeColor = self.strokeColor + axis.strokeWidth = self.strokeWidth + axis.strokeDashArray = self.strokeDashArray + g.add(axis) + + return g + + def makeTicks(self): + g = Group() + if self.visibleTicks and (self.tickUp or self.tickDown): + self._makeLines(g,-self.tickDown,self.tickUp,self.strokeColor,self.strokeWidth,self.strokeDashArray) + return g + +class NormalDateXValueAxis(XValueAxis): + """An X axis applying additional rules. + + Depending on the data and some built-in rules, the axis + displays normalDate values as nicely formatted dates. + + The client chart should have NormalDate X values. + """ + + _attrMap = AttrMap(BASE = XValueAxis, + bottomAxisLabelSlack = AttrMapValue(isNumber, desc="Fractional amount used to adjust label spacing"), + niceMonth = AttrMapValue(isBoolean, desc="Flag for displaying months 'nicely'."), + forceEndDate = AttrMapValue(isBoolean, desc='Flag for enforced displaying of last date value.'), + forceFirstDate = AttrMapValue(isBoolean, desc='Flag for enforced displaying of first date value.'), + xLabelFormat = AttrMapValue(None, desc="Label format string (e.g. '{mm}/{yy}') or function."), + dayOfWeekName = AttrMapValue(SequenceOf(isString,emptyOK=0,lo=7,hi=7), desc='Weekday names.'), + monthName = AttrMapValue(SequenceOf(isString,emptyOK=0,lo=12,hi=12), desc='Month names.'), + dailyFreq = AttrMapValue(isBoolean, desc='True if we are to assume daily data to be ticked at end of month.'), + ) + + _valueClass = normalDate.ND + + def __init__(self, **kw): + apply(XValueAxis.__init__, (self,)) + + # some global variables still used... + self.bottomAxisLabelSlack = 0.1 + self.niceMonth = 1 + self.forceEndDate = 0 + self.forceFirstDate = 0 + self.dailyFreq = 0 + self.xLabelFormat = "{mm}/{yy}" + self.dayOfWeekName = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] + self.monthName = ['January', 'February', 'March', 'April', 'May', 'June', 'July', + 'August', 'September', 'October', 'November', 'December'] + self.valueSteps = None + + def _scalar2ND(self, x): + "Convert a scalar to a NormalDate value." + d = self._valueClass() + d.normalize(x) + return d + + def _dateFormatter(self, v): + "Create a formatted label for some value." + if not isinstance(v,normalDate.NormalDate): + v = self._scalar2ND(v) + d, m = normalDate._dayOfWeekName, normalDate._monthName + try: + normalDate._dayOfWeekName, normalDate._monthName = self.dayOfWeekName, self.monthName + return v.formatMS(self.xLabelFormat) + finally: + normalDate._dayOfWeekName, normalDate._monthName = d, m + + def _xAxisTicker(self, xVals): + """Complex stuff... + + Needs explanation... + """ + axisLength = self._length + formatter = self._dateFormatter + labels = self.labels + fontName, fontSize, leading = labels.fontName, labels.fontSize, labels.leading + textAnchor, boxAnchor, angle = labels.textAnchor, labels.boxAnchor, labels.angle + RBL = _textBoxLimits(string.split(formatter(xVals[0]),'\n'),fontName, + fontSize,leading or 1.2*fontSize,textAnchor,boxAnchor) + RBL = _rotatedBoxLimits(RBL[0],RBL[1],RBL[2],RBL[3], angle) + xLabelW = RBL[1]-RBL[0] + xLabelH = RBL[3]-RBL[2] + w = max(xLabelW,labels.width,self.minimumTickSpacing) + + W = w+w*self.bottomAxisLabelSlack + n = len(xVals) + ticks = [] + labels = [] + maximumTicks = self.maximumTicks + + def addTick(i, xVals=xVals, formatter=formatter, ticks=ticks, labels=labels): + ticks.insert(0,xVals[i]) + labels.insert(0,formatter(xVals[i])) + + for d in (1,2,3,6,12,24,60,120): + k = n/d + if k<=maximumTicks and k*W <= axisLength: + i = n-1 + if self.niceMonth: + j = xVals[-1].month() % (d<=12 and d or 12) + if j: + if self.forceEndDate: addTick(i) + i = i - j + + #weird first date ie not at end of month + try: + wfd = xVals[0].month() == xVals[1].month() + except: + wfd = 0 + + while i>=wfd: + addTick(i) + i = i - d + + if self.forceFirstDate and ticks[0] != xVals[0]: + addTick(0) + if (axisLength/(ticks[-1]-ticks[0]))*(ticks[1]-ticks[0])<=w: + del ticks[1], labels[1] + if self.forceEndDate and self.niceMonth and j: + if (axisLength/(ticks[-1]-ticks[0]))*(ticks[-1]-ticks[-2])<=w: + del ticks[-2], labels[-2] + try: + if labels[0] and labels[0]==labels[1]: + del ticks[1], labels[1] + except IndexError: + pass + + return ticks, labels + + def _convertXV(self,data): + '''Convert all XValues to a standard normalDate type''' + + VC = self._valueClass + for D in data: + for i in xrange(len(D)): + x, y = D[i] + if not isinstance(x,VC): + D[i] = (VC(x),y) + + def _getStepsAndLabels(self,xVals): + if self.dailyFreq: + xEOM = [] + pm = 0 + px = xVals[0] + for x in xVals: + m = x.month() + if pm!=m: + if pm: xEOM.append(px) + pm = m + px = x + px = xVals[-1] + if xEOM[-1]!=x: xEOM.append(px) + steps, labels = self._xAxisTicker(xEOM) + else: + steps, labels = self._xAxisTicker(xVals) + return steps, labels + + def configure(self, data): + self._convertXV(data) + from reportlab.lib.set_ops import union + xVals = reduce(union,map(lambda x: map(lambda dv: dv[0],x),data),[]) + xVals.sort() + steps,labels = self._getStepsAndLabels(xVals) + valueMin, valueMax = self.valueMin, self.valueMax + if valueMin is None: valueMin = xVals[0] + if valueMax is None: valueMax = xVals[-1] + self._valueMin, self._valueMax = valueMin, valueMax + self._tickValues = steps + self._labelTextFormat = labels + + self._scaleFactor = self._length / float(valueMax - valueMin) + self._tickValues = steps + self._configured = 1 + +class YValueAxis(ValueAxis): + "Y/value axis" + + _attrMap = AttrMap(BASE=ValueAxis, + tickLeft = AttrMapValue(isNumber, + desc='Tick length left of the axis.'), + tickRight = AttrMapValue(isNumber, + desc='Tick length right of the axis.'), + joinAxis = AttrMapValue(None, + desc='Join both axes if true.'), + joinAxisMode = AttrMapValue(OneOf(('left', 'right', 'value', 'points', None)), + desc="Mode used for connecting axis ('left', 'right', 'value', 'points', None)."), + joinAxisPos = AttrMapValue(isNumberOrNone, + desc='Position at which to join with other axis.'), + ) + + # Indicate the dimension of the data we're interested in. + _dataIndex = 1 + + def __init__(self): + ValueAxis.__init__(self) + + self.labels.boxAnchor = 'e' + self.labels.dx = -5 + self.labels.dy = 0 + + self.tickRight = 0 + self.tickLeft = 5 + + self.joinAxis = None + self.joinAxisMode = None + self.joinAxisPos = None + + + def demo(self): + data = [(10, 20, 30, 42)] + self.setPosition(100, 10, 80) + self.configure(data) + + drawing = Drawing(200, 100) + drawing.add(self) + return drawing + + + + def joinToAxis(self, xAxis, mode='left', pos=None): + "Join with x-axis using some mode." + _assertXAxis(xAxis) + if mode == 'left': + self._x = xAxis._x * 1.0 + self._y = xAxis._y * 1.0 + elif mode == 'right': + self._x = (xAxis._x + xAxis._length) * 1.0 + self._y = xAxis._y * 1.0 + elif mode == 'value': + self._x = xAxis.scale(pos) * 1.0 + self._y = xAxis._y * 1.0 + elif mode == 'points': + self._x = pos * 1.0 + self._y = xAxis._y * 1.0 + + def _joinToAxis(self): + ja = self.joinAxis + if ja: + jam = self.joinAxisMode + jap = self.joinAxisPos + jta = self.joinToAxis + if jam in ('left', 'right'): + jta(ja, mode=jam) + elif jam in ('value', 'points'): + jta(ja, mode=jam, pos=jap) + + def scale(self, value): + """Converts a numeric value to a Y position. + + The chart first configures the axis, then asks it to + work out the x value for each point when plotting + lines or bars. You could override this to do + logarithmic axes. + """ + + msg = "Axis cannot scale numbers before it is configured" + assert self._configured, msg + + if value is None: + value = 0 + return self._y + self._scaleFactor * (value - self._valueMin) + + + def makeAxis(self): + g = Group() + self._joinToAxis() + if not self.visibleAxis: return g + + axis = Line(self._x, self._y, self._x, self._y + self._length) + axis.strokeColor = self.strokeColor + axis.strokeWidth = self.strokeWidth + axis.strokeDashArray = self.strokeDashArray + g.add(axis) + + return g + + def makeTicks(self): + g = Group() + if self.visibleTicks and (self.tickLeft or self.tickRight): + self._makeLines(g,-self.tickLeft,self.tickRight,self.strokeColor,self.strokeWidth,self.strokeDashArray) + return g + +class AdjYValueAxis(YValueAxis): + """A Y-axis applying additional rules. + + Depending on the data and some built-in rules, the axis + may choose to adjust its range and origin. + """ + _attrMap = AttrMap(BASE = YValueAxis, + requiredRange = AttrMapValue(isNumberOrNone, desc='Minimum required value range.'), + leftAxisPercent = AttrMapValue(isBoolean, desc='When true add percent sign to label values.'), + leftAxisOrigShiftIPC = AttrMapValue(isNumber, desc='Lowest label shift interval ratio.'), + leftAxisOrigShiftMin = AttrMapValue(isNumber, desc='Minimum amount to shift.'), + leftAxisSkipLL0 = AttrMapValue(EitherOr((isBoolean,isListOfNumbers)), desc='Skip/Keep lowest tick label when true/false.\nOr skiplist') + ) + + def __init__(self): + apply(YValueAxis.__init__, (self,)) + self.requiredRange = 30 + self.leftAxisPercent = 1 + self.leftAxisOrigShiftIPC = 0.15 + self.leftAxisOrigShiftMin = 12 + self.leftAxisSkipLL0 = 0 + self.valueSteps = None + + def _rangeAdjust(self): + "Adjusts the value range of the axis." + + from reportlab.graphics.charts.utils import find_good_grid, ticks + y_min, y_max = self._valueMin, self._valueMax + m = self.maximumTicks + n = filter(lambda x,m=m: x<=m,[4,5,6,7,8,9]) + if not n: n = [m] + + valueStep, requiredRange = self.valueStep, self.requiredRange + if requiredRange and y_max - y_min < requiredRange: + y1, y2 = find_good_grid(y_min, y_max,n=n,grid=valueStep)[:2] + if y2 - y1 < requiredRange: + ym = (y1+y2)*0.5 + y1 = min(ym-requiredRange*0.5,y_min) + y2 = max(ym+requiredRange*0.5,y_max) + if y_min>=100 and y1<100: + y2 = y2 + 100 - y1 + y1 = 100 + elif y_min>=0 and y1<0: + y2 = y2 - y1 + y1 = 0 + self._valueMin, self._valueMax = y1, y2 + + T, L = ticks(self._valueMin, self._valueMax, split=1, n=n, percent=self.leftAxisPercent,grid=valueStep) + abf = self.avoidBoundFrac + if abf: + i1 = (T[1]-T[0]) + if type(abf) not in (TupleType,ListType): + i0 = i1 = i1*abf + else: + i0 = i1*abf[0] + i1 = i1*abf[1] + _n = getattr(self,'_cValueMin',T[0]) + _x = getattr(self,'_cValueMax',T[-1]) + if _n - T[0] < i0: self._valueMin = self._valueMin - i0 + if T[-1]-_x < i1: self._valueMax = self._valueMax + i1 + T, L = ticks(self._valueMin, self._valueMax, split=1, n=n, percent=self.leftAxisPercent,grid=valueStep) + + self._valueMin = T[0] + self._valueMax = T[-1] + self._tickValues = self.valueSteps = T + if self.labelTextFormat is None: + self._labelTextFormat = L + else: + self._labelTextFormat = self.labelTextFormat + + if abs(self._valueMin-100)<1e-6: + self._calcValueStep() + vMax, vMin = self._valueMax, self._valueMin + m = max(self.leftAxisOrigShiftIPC*self._valueStep, + (vMax-vMin)*self.leftAxisOrigShiftMin/self._length) + self._valueMin = self._valueMin - m + + if self.leftAxisSkipLL0: + if type(self.leftAxisSkipLL0) in (ListType,TupleType): + for x in self.leftAxisSkipLL0: + try: + L[x] = '' + except IndexError: + pass + L[0] = '' + +# Sample functions. +def sample0a(): + "Sample drawing with one xcat axis and two buckets." + + drawing = Drawing(400, 200) + + data = [(10, 20)] + + xAxis = XCategoryAxis() + xAxis.setPosition(75, 75, 300) + xAxis.configure(data) + xAxis.categoryNames = ['Ying', 'Yang'] + xAxis.labels.boxAnchor = 'n' + + drawing.add(xAxis) + + return drawing + + +def sample0b(): + "Sample drawing with one xcat axis and one bucket only." + + drawing = Drawing(400, 200) + + data = [(10,)] + + xAxis = XCategoryAxis() + xAxis.setPosition(75, 75, 300) + xAxis.configure(data) + xAxis.categoryNames = ['Ying'] + xAxis.labels.boxAnchor = 'n' + + drawing.add(xAxis) + + return drawing + + +def sample1(): + "Sample drawing containing two unconnected axes." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + xAxis = XCategoryAxis() + xAxis.setPosition(75, 75, 300) + xAxis.configure(data) + xAxis.categoryNames = ['Beer','Wine','Meat','Cannelloni'] + xAxis.labels.boxAnchor = 'n' + xAxis.labels[3].dy = -15 + xAxis.labels[3].angle = 30 + xAxis.labels[3].fontName = 'Times-Bold' + + yAxis = YValueAxis() + yAxis.setPosition(50, 50, 125) + yAxis.configure(data) + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing + + +##def sample2a(): +## "Make sample drawing with two axes, x connected at top of y." +## +## drawing = Drawing(400, 200) +## +## data = [(10, 20, 30, 42)] +## +## yAxis = YValueAxis() +## yAxis.setPosition(50, 50, 125) +## yAxis.configure(data) +## +## xAxis = XCategoryAxis() +## xAxis._length = 300 +## xAxis.configure(data) +## xAxis.joinToAxis(yAxis, mode='top') +## xAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni'] +## xAxis.labels.boxAnchor = 'n' +## +## drawing.add(xAxis) +## drawing.add(yAxis) +## +## return drawing +## +## +##def sample2b(): +## "Make two axes, x connected at bottom of y." +## +## drawing = Drawing(400, 200) +## +## data = [(10, 20, 30, 42)] +## +## yAxis = YValueAxis() +## yAxis.setPosition(50, 50, 125) +## yAxis.configure(data) +## +## xAxis = XCategoryAxis() +## xAxis._length = 300 +## xAxis.configure(data) +## xAxis.joinToAxis(yAxis, mode='bottom') +## xAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni'] +## xAxis.labels.boxAnchor = 'n' +## +## drawing.add(xAxis) +## drawing.add(yAxis) +## +## return drawing +## +## +##def sample2c(): +## "Make two axes, x connected at fixed value (in points) of y." +## +## drawing = Drawing(400, 200) +## +## data = [(10, 20, 30, 42)] +## +## yAxis = YValueAxis() +## yAxis.setPosition(50, 50, 125) +## yAxis.configure(data) +## +## xAxis = XCategoryAxis() +## xAxis._length = 300 +## xAxis.configure(data) +## xAxis.joinToAxis(yAxis, mode='points', pos=100) +## xAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni'] +## xAxis.labels.boxAnchor = 'n' +## +## drawing.add(xAxis) +## drawing.add(yAxis) +## +## return drawing +## +## +##def sample2d(): +## "Make two axes, x connected at fixed value (of y-axes) of y." +## +## drawing = Drawing(400, 200) +## +## data = [(10, 20, 30, 42)] +## +## yAxis = YValueAxis() +## yAxis.setPosition(50, 50, 125) +## yAxis.configure(data) +## +## xAxis = XCategoryAxis() +## xAxis._length = 300 +## xAxis.configure(data) +## xAxis.joinToAxis(yAxis, mode='value', pos=20) +## xAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni'] +## xAxis.labels.boxAnchor = 'n' +## +## drawing.add(xAxis) +## drawing.add(yAxis) +## +## return drawing +## +## +##def sample3a(): +## "Make sample drawing with two axes, y connected at left of x." +## +## drawing = Drawing(400, 200) +## +## data = [(10, 20, 30, 42)] +## +## xAxis = XCategoryAxis() +## xAxis._length = 300 +## xAxis.configure(data) +## xAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni'] +## xAxis.labels.boxAnchor = 'n' +## +## yAxis = YValueAxis() +## yAxis.setPosition(50, 50, 125) +## yAxis.configure(data) +## yAxis.joinToAxis(xAxis, mode='left') +## +## drawing.add(xAxis) +## drawing.add(yAxis) +## +## return drawing +## +## +##def sample3b(): +## "Make sample drawing with two axes, y connected at right of x." +## +## drawing = Drawing(400, 200) +## +## data = [(10, 20, 30, 42)] +## +## xAxis = XCategoryAxis() +## xAxis._length = 300 +## xAxis.configure(data) +## xAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni'] +## xAxis.labels.boxAnchor = 'n' +## +## yAxis = YValueAxis() +## yAxis.setPosition(50, 50, 125) +## yAxis.configure(data) +## yAxis.joinToAxis(xAxis, mode='right') +## +## drawing.add(xAxis) +## drawing.add(yAxis) +## +## return drawing +## +## +##def sample3c(): +## "Make two axes, y connected at fixed value (in points) of x." +## +## drawing = Drawing(400, 200) +## +## data = [(10, 20, 30, 42)] +## +## yAxis = YValueAxis() +## yAxis.setPosition(50, 50, 125) +## yAxis.configure(data) +## +## xAxis = XValueAxis() +## xAxis._length = 300 +## xAxis.configure(data) +## xAxis.joinToAxis(yAxis, mode='points', pos=100) +## +## drawing.add(xAxis) +## drawing.add(yAxis) +## +## return drawing + + +def sample4a(): + "Sample drawing, xvalue/yvalue axes, y connected at 100 pts to x." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + yAxis = YValueAxis() + yAxis.setPosition(50, 50, 125) + yAxis.configure(data) + + xAxis = XValueAxis() + xAxis._length = 300 + xAxis.joinAxis = yAxis + xAxis.joinAxisMode = 'points' + xAxis.joinAxisPos = 100 + xAxis.configure(data) + + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing + + +def sample4b(): + "Sample drawing, xvalue/yvalue axes, y connected at value 35 of x." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + yAxis = YValueAxis() + yAxis.setPosition(50, 50, 125) + yAxis.configure(data) + + xAxis = XValueAxis() + xAxis._length = 300 + xAxis.joinAxis = yAxis + xAxis.joinAxisMode = 'value' + xAxis.joinAxisPos = 35 + xAxis.configure(data) + + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing + + +def sample4c(): + "Sample drawing, xvalue/yvalue axes, y connected to bottom of x." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + yAxis = YValueAxis() + yAxis.setPosition(50, 50, 125) + yAxis.configure(data) + + xAxis = XValueAxis() + xAxis._length = 300 + xAxis.joinAxis = yAxis + xAxis.joinAxisMode = 'bottom' + xAxis.configure(data) + + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing + + +def sample4c1(): + "xvalue/yvalue axes, without drawing axis lines/ticks." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + yAxis = YValueAxis() + yAxis.setPosition(50, 50, 125) + yAxis.configure(data) + yAxis.visibleAxis = 0 + yAxis.visibleTicks = 0 + + xAxis = XValueAxis() + xAxis._length = 300 + xAxis.joinAxis = yAxis + xAxis.joinAxisMode = 'bottom' + xAxis.configure(data) + xAxis.visibleAxis = 0 + xAxis.visibleTicks = 0 + + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing + + +def sample4d(): + "Sample drawing, xvalue/yvalue axes, y connected to top of x." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + yAxis = YValueAxis() + yAxis.setPosition(50, 50, 125) + yAxis.configure(data) + + xAxis = XValueAxis() + xAxis._length = 300 + xAxis.joinAxis = yAxis + xAxis.joinAxisMode = 'top' + xAxis.configure(data) + + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing + + +def sample5a(): + "Sample drawing, xvalue/yvalue axes, y connected at 100 pts to x." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + xAxis = XValueAxis() + xAxis.setPosition(50, 50, 300) + xAxis.configure(data) + + yAxis = YValueAxis() + yAxis.setPosition(50, 50, 125) + yAxis.joinAxis = xAxis + yAxis.joinAxisMode = 'points' + yAxis.joinAxisPos = 100 + yAxis.configure(data) + + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing + + +def sample5b(): + "Sample drawing, xvalue/yvalue axes, y connected at value 35 of x." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + xAxis = XValueAxis() + xAxis.setPosition(50, 50, 300) + xAxis.configure(data) + + yAxis = YValueAxis() + yAxis.setPosition(50, 50, 125) + yAxis.joinAxis = xAxis + yAxis.joinAxisMode = 'value' + yAxis.joinAxisPos = 35 + yAxis.configure(data) + + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing + + +def sample5c(): + "Sample drawing, xvalue/yvalue axes, y connected at right of x." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + xAxis = XValueAxis() + xAxis.setPosition(50, 50, 300) + xAxis.configure(data) + + yAxis = YValueAxis() + yAxis.setPosition(50, 50, 125) + yAxis.joinAxis = xAxis + yAxis.joinAxisMode = 'right' + yAxis.configure(data) + + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing + + +def sample5d(): + "Sample drawing, xvalue/yvalue axes, y connected at left of x." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + xAxis = XValueAxis() + xAxis.setPosition(50, 50, 300) + xAxis.configure(data) + + yAxis = YValueAxis() + yAxis.setPosition(50, 50, 125) + yAxis.joinAxis = xAxis + yAxis.joinAxisMode = 'left' + yAxis.configure(data) + + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing + + +def sample6a(): + "Sample drawing, xcat/yvalue axes, x connected at top of y." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + yAxis = YValueAxis() + yAxis.setPosition(50, 50, 125) + yAxis.configure(data) + + xAxis = XCategoryAxis() + xAxis._length = 300 + xAxis.configure(data) + xAxis.joinAxis = yAxis + xAxis.joinAxisMode = 'top' + xAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni'] + xAxis.labels.boxAnchor = 'n' + + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing + + +def sample6b(): + "Sample drawing, xcat/yvalue axes, x connected at bottom of y." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + yAxis = YValueAxis() + yAxis.setPosition(50, 50, 125) + yAxis.configure(data) + + xAxis = XCategoryAxis() + xAxis._length = 300 + xAxis.configure(data) + xAxis.joinAxis = yAxis + xAxis.joinAxisMode = 'bottom' + xAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni'] + xAxis.labels.boxAnchor = 'n' + + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing + + +def sample6c(): + "Sample drawing, xcat/yvalue axes, x connected at 100 pts to y." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + yAxis = YValueAxis() + yAxis.setPosition(50, 50, 125) + yAxis.configure(data) + + xAxis = XCategoryAxis() + xAxis._length = 300 + xAxis.configure(data) + xAxis.joinAxis = yAxis + xAxis.joinAxisMode = 'points' + xAxis.joinAxisPos = 100 + xAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni'] + xAxis.labels.boxAnchor = 'n' + + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing + + +def sample6d(): + "Sample drawing, xcat/yvalue axes, x connected at value 20 of y." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + yAxis = YValueAxis() + yAxis.setPosition(50, 50, 125) + yAxis.configure(data) + + xAxis = XCategoryAxis() + xAxis._length = 300 + xAxis.configure(data) + xAxis.joinAxis = yAxis + xAxis.joinAxisMode = 'value' + xAxis.joinAxisPos = 20 + xAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni'] + xAxis.labels.boxAnchor = 'n' + + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing + + +def sample7a(): + "Sample drawing, xvalue/ycat axes, y connected at right of x." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + xAxis = XValueAxis() + xAxis._length = 300 + xAxis.configure(data) + + yAxis = YCategoryAxis() + yAxis.setPosition(50, 50, 125) + yAxis.joinAxis = xAxis + yAxis.joinAxisMode = 'right' + yAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni'] + yAxis.labels.boxAnchor = 'e' + yAxis.configure(data) + + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing + + +def sample7b(): + "Sample drawing, xvalue/ycat axes, y connected at left of x." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + xAxis = XValueAxis() + xAxis._length = 300 + xAxis.configure(data) + + yAxis = YCategoryAxis() + yAxis.setPosition(50, 50, 125) + yAxis.joinAxis = xAxis + yAxis.joinAxisMode = 'left' + yAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni'] + yAxis.labels.boxAnchor = 'e' + yAxis.configure(data) + + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing + + +def sample7c(): + "Sample drawing, xvalue/ycat axes, y connected at value 30 of x." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + xAxis = XValueAxis() + xAxis._length = 300 + xAxis.configure(data) + + yAxis = YCategoryAxis() + yAxis.setPosition(50, 50, 125) + yAxis.joinAxis = xAxis + yAxis.joinAxisMode = 'value' + yAxis.joinAxisPos = 30 + yAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni'] + yAxis.labels.boxAnchor = 'e' + yAxis.configure(data) + + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing + + +def sample7d(): + "Sample drawing, xvalue/ycat axes, y connected at 200 pts to x." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + xAxis = XValueAxis() + xAxis._length = 300 + xAxis.configure(data) + + yAxis = YCategoryAxis() + yAxis.setPosition(50, 50, 125) + yAxis.joinAxis = xAxis + yAxis.joinAxisMode = 'points' + yAxis.joinAxisPos = 200 + yAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni'] + yAxis.labels.boxAnchor = 'e' + yAxis.configure(data) + + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing diff --git a/bin/reportlab/graphics/charts/barcharts.py b/bin/reportlab/graphics/charts/barcharts.py new file mode 100644 index 00000000000..0ca1ae3ed57 --- /dev/null +++ b/bin/reportlab/graphics/charts/barcharts.py @@ -0,0 +1,1972 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/barcharts.py +"""This module defines a variety of Bar Chart components. + +The basic flavors are Side-by-side, available in horizontal and +vertical versions. + +Stacked and percentile bar charts to follow... +""" +__version__=''' $Id: barcharts.py 2647 2005-07-26 13:47:51Z rgbecker $ ''' + +import string, copy +from types import FunctionType, StringType + +from reportlab.lib import colors +from reportlab.lib.validators import isNumber, isColor, isColorOrNone, isString,\ + isListOfStrings, SequenceOf, isBoolean, isNoneOrShape, isStringOrNone,\ + NoneOr +from reportlab.graphics.widgets.markers import uSymbol2Symbol, isSymbol +from reportlab.lib.formatters import Formatter +from reportlab.lib.attrmap import AttrMap, AttrMapValue +from reportlab.pdfbase.pdfmetrics import stringWidth +from reportlab.graphics.widgetbase import Widget, TypedPropertyCollection, PropHolder +from reportlab.graphics.shapes import Line, Rect, Group, Drawing, NotImplementedError +from reportlab.graphics.charts.axes import XCategoryAxis, YValueAxis, YCategoryAxis, XValueAxis +from reportlab.graphics.charts.textlabels import BarChartLabel, NA_Label, NoneOrInstanceOfNA_Label +from reportlab.graphics.charts.areas import PlotArea + +class BarChartProperties(PropHolder): + _attrMap = AttrMap( + strokeColor = AttrMapValue(isColorOrNone, desc='Color of the bar border.'), + fillColor = AttrMapValue(isColorOrNone, desc='Color of the bar interior area.'), + strokeWidth = AttrMapValue(isNumber, desc='Width of the bar border.'), + symbol = AttrMapValue(None, desc='A widget to be used instead of a normal bar.'), + name = AttrMapValue(isString, desc='Text to be associated with a bar (eg seriesname)'), + swatchMarker = AttrMapValue(NoneOr(isSymbol), desc="None or makeMarker('Diamond') ..."), + ) + + def __init__(self): + self.strokeColor = None + self.fillColor = colors.blue + self.strokeWidth = 0.5 + self.symbol = None + +# Bar chart classes. +class BarChart(PlotArea): + "Abstract base class, unusable by itself." + + _attrMap = AttrMap(BASE=PlotArea, + useAbsolute = AttrMapValue(isNumber, desc='Flag to use absolute spacing values.'), + barWidth = AttrMapValue(isNumber, desc='The width of an individual bar.'), + groupSpacing = AttrMapValue(isNumber, desc='Width between groups of bars.'), + barSpacing = AttrMapValue(isNumber, desc='Width between individual bars.'), + bars = AttrMapValue(None, desc='Handle of the individual bars.'), + valueAxis = AttrMapValue(None, desc='Handle of the value axis.'), + categoryAxis = AttrMapValue(None, desc='Handle of the category axis.'), + data = AttrMapValue(None, desc='Data to be plotted, list of (lists of) numbers.'), + barLabels = AttrMapValue(None, desc='Handle to the list of bar labels.'), + barLabelFormat = AttrMapValue(None, desc='Formatting string or function used for bar labels.'), + barLabelCallOut = AttrMapValue(None, desc='Callout function(label)\nlabel._callOutInfo = (self,g,rowNo,colNo,x,y,width,height,x00,y00,x0,y0)'), + barLabelArray = AttrMapValue(None, desc='explicit array of bar label values, must match size of data if present.'), + reversePlotOrder = AttrMapValue(isBoolean, desc='If true, reverse common category plot order.'), + naLabel = AttrMapValue(NoneOrInstanceOfNA_Label, desc='Label to use for N/A values.'), + annotations = AttrMapValue(None, desc='list of callables, will be called with self, xscale, yscale.'), + ) + + def makeSwatchSample(self, rowNo, x, y, width, height): + baseStyle = self.bars + 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(style, 'strokeWidth',None)) + swatchMarker = getattr(style, 'swatchMarker', getattr(baseStyle, 'swatchMarker',None)) + if swatchMarker: + return uSymbol2Symbol(swatchMarker,x+width/2.,y+height/2.,fillColor) + return Rect(x,y,width,height,strokeWidth=strokeWidth,strokeColor=strokeColor, + strokeDashArray=strokeDashArray,fillColor=fillColor) + + def getSeriesName(self,i,default=None): + '''return series name i or default''' + return getattr(self.bars[i],'name',default) + + def __init__(self): + assert self.__class__.__name__ not in ('BarChart','BarChart3D'), 'Abstract Class %s Instantiated' % self.__class__.__name__ + + if self._flipXY: + self.categoryAxis = YCategoryAxis() + self.valueAxis = XValueAxis() + else: + self.categoryAxis = XCategoryAxis() + self.valueAxis = YValueAxis() + + PlotArea.__init__(self) + self.barSpacing = 0 + self.reversePlotOrder = 0 + + + # this defines two series of 3 points. Just an example. + self.data = [(100,110,120,130), + (70, 80, 85, 90)] + + # control bar spacing. is useAbsolute = 1 then + # the next parameters are in points; otherwise + # they are 'proportions' and are normalized to + # fit the available space. Half a barSpacing + # is allocated at the beginning and end of the + # chart. + self.useAbsolute = 0 #- not done yet + self.barWidth = 10 + self.groupSpacing = 5 + self.barSpacing = 0 + + self.barLabels = TypedPropertyCollection(BarChartLabel) + self.barLabels.boxAnchor = 'c' + self.barLabels.textAnchor = 'middle' + self.barLabelFormat = None + self.barLabelArray = None + # this says whether the origin is inside or outside + # the bar - +10 means put the origin ten points + # above the tip of the bar if value > 0, or ten + # points inside if bar value < 0. This is different + # to label dx/dy which are not dependent on the + # sign of the data. + self.barLabels.nudge = 0 + + # if you have multiple series, by default they butt + # together. + + # we really need some well-designed default lists of + # colors e.g. from Tufte. These will be used in a + # cycle to set the fill color of each series. + self.bars = TypedPropertyCollection(BarChartProperties) +## self.bars.symbol = None + self.bars.strokeWidth = 1 + self.bars.strokeColor = colors.black + + self.bars[0].fillColor = colors.red + self.bars[1].fillColor = colors.green + self.bars[2].fillColor = colors.blue + self.naLabel = None#NA_Label() + + + def demo(self): + """Shows basic use of a bar chart""" + if self.__class__.__name__=='BarChart': + raise NotImplementedError, 'Abstract Class BarChart has no demo' + drawing = Drawing(200, 100) + bc = self.__class__() + drawing.add(bc) + return drawing + + def _getConfigureData(self): + cA = self.categoryAxis + data = self.data + if cA.style not in ('parallel','parallel_3d'): + _data = data + data = max(map(len,_data))*[0] + for d in _data: + for i in xrange(len(d)): + data[i] = data[i] + (d[i] or 0) + data = list(_data) + [data] + self._configureData = data + + def _getMinMax(self): + '''Attempt to return the data range''' + self._getConfigureData() + self.valueAxis._setRange(self._configureData) + return self.valueAxis._valueMin, self.valueAxis._valueMax + + def _drawBegin(self,org,length): + '''Position and configure value axis, return crossing value''' + vA = self.valueAxis + vA.setPosition(self.x, self.y, length) + self._getConfigureData() + vA.configure(self._configureData) + + # if zero is in chart, put the other axis there, otherwise use low + crossesAt = vA.scale(0) + if crossesAt > org+length or crossesAt<org: + crossesAt = org + return crossesAt + + def _drawFinish(self): + '''finalize the drawing of a barchart''' + cA = self.categoryAxis + vA = self.valueAxis + cA.configure(self._configureData) + self.calcBarPositions() + g = Group() + g.add(self.makeBackground()) + + # ensure any axes have correct spacing set + # for grids. It sucks that we have to do + # this here. + if self._flipXY == 0: + vA.gridStart = cA._x + vA.gridEnd = cA._x+cA._length + cA.gridStart = vA._y + cA.gridEnd = vA._y+vA._length + else: + cA.gridStart = vA._x + cA.gridEnd = vA._x+vA._length + vA.gridStart = cA._y + vA.gridEnd = cA._y+cA._length + + cA.makeGrid(g,parent=self) + vA.makeGrid(g,parent=self) + g.add(self.makeBars()) + g.add(cA) + g.add(vA) + for a in getattr(self,'annotations',()): g.add(a(self,cA.scale,vA.scale)) + del self._configureData + return g + + + def calcBarPositions(self): + """Works out where they go. default vertical. + + Sets an attribute _barPositions which is a list of + lists of (x, y, width, height) matching the data. + """ + + flipXY = self._flipXY + if flipXY: + org = self.y + else: + org = self.x + cA = self.categoryAxis + cScale = cA.scale + + data = self.data + seriesCount = self._seriesCount = len(data) + self._rowLength = rowLength = max(map(len,data)) + groupSpacing, barSpacing, barWidth = self.groupSpacing, self.barSpacing, self.barWidth + style = self.categoryAxis.style + if style=='parallel': + groupWidth = groupSpacing+(seriesCount*barWidth)+(seriesCount-1)*barSpacing + bGap = barWidth+barSpacing + else: + accum = rowLength*[0] + groupWidth = groupSpacing+barWidth + bGap = 0 + self._groupWidth = groupWidth + useAbsolute = self.useAbsolute + + if useAbsolute: + # bar dimensions are absolute + normFactor = 1.0 + else: + # bar dimensions are normalized to fit. How wide + # notionally is one group of bars? + availWidth = cScale(0)[1] + normFactor = availWidth/float(groupWidth) + if self.debug: + print '%d series, %d points per series' % (seriesCount, self._rowLength) + print 'width = %d group + (%d bars * %d barWidth) + (%d gaps * %d interBar) = %d total' % ( + groupSpacing, seriesCount, barWidth, + seriesCount-1, barSpacing, groupWidth) + + # 'Baseline' correction... + vA = self.valueAxis + vScale = vA.scale + vm, vM = vA._valueMin, vA._valueMax + if vm <= 0 <= vM: + baseLine = vScale(0) + elif 0 < vm: + baseLine = vScale(vm) + elif vM < 0: + baseLine = vScale(vM) + self._baseLine = baseLine + + COLUMNS = range(max(map(len,data))) + if useAbsolute: + _cScale = cA._scale + + self._normFactor = normFactor + width = self.barWidth*normFactor + self._barPositions = [] + reversePlotOrder = self.reversePlotOrder + for rowNo in range(seriesCount): + barRow = [] + if reversePlotOrder: + xVal = seriesCount-1 - rowNo + else: + xVal = rowNo + xVal = 0.5*groupSpacing+xVal*bGap + for colNo in COLUMNS: + datum = data[rowNo][colNo] + + # Ufff... + if useAbsolute: + x = groupWidth*_cScale(colNo) + xVal + org + else: + (g, gW) = cScale(colNo) + x = g + normFactor*xVal + + if datum is None: + height = None + y = baseLine + else: + if style not in ('parallel','parallel_3d'): + y = vScale(accum[colNo]) + if y<baseLine: y = baseLine + accum[colNo] = accum[colNo] + datum + datum = accum[colNo] + else: + y = baseLine + height = vScale(datum) - y + if -1e-8<height<=1e-8: + height = 1e-8 + if datum<-1e-8: height = -1e-8 + barRow.append(flipXY and (y,x,height,width) or (x, y, width, height)) + + self._barPositions.append(barRow) + + + def _getLabelText(self, rowNo, colNo): + '''return formatted label text''' + labelFmt = self.barLabelFormat + if labelFmt is None: + labelText = None + elif labelFmt == 'values': + labelText = self.barLabelArray[rowNo][colNo] + elif type(labelFmt) is StringType: + labelText = labelFmt % self.data[rowNo][colNo] + elif callable(labelFmt): + labelText = labelFmt(self.data[rowNo][colNo]) + else: + msg = "Unknown formatter type %s, expected string or function" % labelFmt + raise Exception, msg + return labelText + + def _labelXY(self,label,x,y,width,height): + 'Compute x, y for a label' + nudge = label.nudge + anti = getattr(label,'boxTarget','normal')=='anti' + if anti: nudge = -nudge + if self._flipXY: + value = width + if anti: value = 0 + return x + value + (width>=0 and 1 or -1)*nudge, y + 0.5*height + else: + value = height + if anti: value = 0 + return x + 0.5*width, y + value + (height>=0 and 1 or -1)*nudge + + def _addBarLabel(self, g, rowNo, colNo, x, y, width, height): + text = self._getLabelText(rowNo,colNo) + if text: + self._addLabel(text, self.barLabels[(rowNo, colNo)], g, rowNo, colNo, x, y, width, height) + + def _addNABarLabel(self, g, rowNo, colNo, x, y, width, height): + na = self.naLabel + if na and na.text: + na = copy.copy(na) + v = self.valueAxis._valueMax<=0 and -1e-8 or 1e-8 + if width is None: width = v + if height is None: height = v + self._addLabel(na.text, na, g, rowNo, colNo, x, y, width, height) + + def _addLabel(self, text, label, g, rowNo, colNo, x, y, width, height): + if label.visible: + labelWidth = stringWidth(text, label.fontName, label.fontSize) + x0, y0 = self._labelXY(label,x,y,width,height) + flipXY = self._flipXY + if flipXY: + pm = width + else: + pm = height + label._pmv = pm #the plus minus val + fixedEnd = getattr(label,'fixedEnd', None) + if fixedEnd is not None: + v = fixedEnd._getValue(self,pm) + x00, y00 = x0, y0 + if flipXY: + x0 = v + else: + y0 = v + else: + if flipXY: + x00 = x0 + y00 = y+height/2.0 + else: + x00 = x+width/2.0 + y00 = y0 + fixedStart = getattr(label,'fixedStart', None) + if fixedStart is not None: + v = fixedStart._getValue(self,pm) + if flipXY: + x00 = v + else: + y00 = v + + if pm<0: + if flipXY: + dx = -2*label.dx + dy = 0 + else: + dy = -2*label.dy + dx = 0 + else: + dy = dx = 0 + label.setOrigin(x0+dx, y0+dy) + label.setText(text) + sC, sW = label.lineStrokeColor, label.lineStrokeWidth + if sC and sW: g.insert(0,Line(x00,y00,x0,y0, strokeColor=sC, strokeWidth=sW)) + g.add(label) + alx = getattr(self,'barLabelCallOut',None) + if alx: + label._callOutInfo = (self,g,rowNo,colNo,x,y,width,height,x00,y00,x0,y0) + alx(label) + del label._callOutInfo + + def _makeBar(self,g,x,y,width,height,rowNo,style): + r = Rect(x, y, width, height) + r.strokeWidth = style.strokeWidth + r.fillColor = style.fillColor + r.strokeColor = style.strokeColor + g.add(r) + + def _makeBars(self,g,lg): + lenData = len(self.data) + bars = self.bars + for rowNo in range(lenData): + row = self._barPositions[rowNo] + styleCount = len(bars) + styleIdx = rowNo % styleCount + rowStyle = bars[styleIdx] + for colNo in range(len(row)): + barPos = row[colNo] + style = bars.has_key((styleIdx,colNo)) and bars[(styleIdx,colNo)] or rowStyle + (x, y, width, height) = barPos + if None in (width,height): + self._addNABarLabel(lg,rowNo,colNo,x,y,width,height) + continue + + # Draw a rectangular symbol for each data item, + # or a normal colored rectangle. + symbol = None + if hasattr(style, 'symbol'): + symbol = copy.deepcopy(style.symbol) + elif hasattr(self.bars, 'symbol'): + symbol = self.bars.symbol + + if symbol: + symbol.x = x + symbol.y = y + symbol.width = width + symbol.height = height + g.add(symbol) + elif abs(width)>1e-7 and abs(height)>=1e-7 and (style.fillColor is not None or style.strokeColor is not None): + self._makeBar(g,x,y,width,height,rowNo,style) + + self._addBarLabel(lg,rowNo,colNo,x,y,width,height) + + def makeBars(self): + g = Group() + lg = Group() + self._makeBars(g,lg) + g.add(lg) + return g + + def _desiredCategoryAxisLength(self): + '''for dynamically computing the desired category axis length''' + style = self.categoryAxis.style + data = self.data + n = len(data) + m = max(map(len,data)) + if style=='parallel': + groupWidth = (n-1)*self.barSpacing+n*self.barWidth + else: + groupWidth = self.barWidth + return m*(self.groupSpacing+groupWidth) + + def draw(self): + cA, vA = self.categoryAxis, self.valueAxis + if vA: ovAjA, vA.joinAxis = vA.joinAxis, cA + if cA: ocAjA, cA.joinAxis = cA.joinAxis, vA + if self._flipXY: + cA.setPosition(self._drawBegin(self.x,self.width), self.y, self.height) + else: + cA.setPosition(self.x, self._drawBegin(self.y,self.height), self.width) + return self._drawFinish() + +class VerticalBarChart(BarChart): + "Vertical bar chart with multiple side-by-side bars." + _flipXY = 0 + +class HorizontalBarChart(BarChart): + "Horizontal bar chart with multiple side-by-side bars." + _flipXY = 1 + +class _FakeGroup: + def __init__(self, cmp=None): + self._data = [] + self._cmp = cmp + + def add(self,what): + self._data.append(what) + + def value(self): + return self._data + + def sort(self): + self._data.sort(self._cmp) + +class BarChart3D(BarChart): + _attrMap = AttrMap(BASE=BarChart, + theta_x = AttrMapValue(isNumber, desc='dx/dz'), + theta_y = AttrMapValue(isNumber, desc='dy/dz'), + zDepth = AttrMapValue(isNumber, desc='depth of an individual series'), + zSpace = AttrMapValue(isNumber, desc='z gap around series'), + ) + theta_x = .5 + theta_y = .5 + zDepth = None + zSpace = None + + def calcBarPositions(self): + BarChart.calcBarPositions(self) + seriesCount = self._seriesCount + zDepth = self.zDepth + if zDepth is None: zDepth = self.barWidth + zSpace = self.zSpace + if zSpace is None: zSpace = self.barSpacing + if self.categoryAxis.style=='parallel_3d': + _3d_depth = seriesCount*zDepth+(seriesCount+1)*zSpace + else: + _3d_depth = zDepth + 2*zSpace + _3d_depth *= self._normFactor + self._3d_dx = self.theta_x*_3d_depth + self._3d_dy = self.theta_y*_3d_depth + + def _calc_z0(self,rowNo): + zDepth = self.zDepth + if zDepth is None: zDepth = self.barWidth + zSpace = self.zSpace + if zSpace is None: zSpace = self.barSpacing + if self.categoryAxis.style=='parallel_3d': + z0 = self._normFactor*(rowNo*(zDepth+zSpace)+zSpace) + else: + z0 = self._normFactor*zSpace + return z0 + + def _makeBar(self,g,x,y,width,height,rowNo,style): + zDepth = self.zDepth + if zDepth is None: zDepth = self.barWidth + zSpace = self.zSpace + if zSpace is None: zSpace = self.barSpacing + z0 = self._calc_z0(rowNo) + z1 = z0 + zDepth*self._normFactor + if width<0: + x += width + width = -width + x += z0*self.theta_x + y += z0*self.theta_y + if self._flipXY: + y += zSpace + else: + x += zSpace + g.add((0,z0,z1,x,y,width,height,rowNo,style)) + + def _addBarLabel(self, g, rowNo, colNo, x, y, width, height): + z0 = self._calc_z0(rowNo) + zSpace = self.zSpace + if zSpace is None: zSpace = self.barSpacing + z1 = z0 + x += z0*self.theta_x + y += z0*self.theta_y + if self._flipXY: + y += zSpace + else: + x += zSpace + g.add((1,z0,z1,x,y,width,height,rowNo,colNo)) + + def makeBars(self): + from utils3d import _draw_3d_bar + fg = _FakeGroup(cmp=self._cmpZ) + self._makeBars(fg,fg) + fg.sort() + g = Group() + theta_x = self.theta_x + theta_y = self.theta_y + for t in fg.value(): + if t[0]==1: + z0,z1,x,y,width,height,rowNo,colNo = t[1:] + BarChart._addBarLabel(self,g,rowNo,colNo,x,y,width,height) + elif t[0]==0: + z0,z1,x,y,width,height,rowNo,style = t[1:] + dz = z1 - z0 + _draw_3d_bar(g, x, x+width, y, y+height, dz*theta_x, dz*theta_y, + fillColor=style.fillColor, fillColorShaded=None, + strokeColor=style.strokeColor, strokeWidth=style.strokeWidth, + shading=0.45) + return g + +class VerticalBarChart3D(BarChart3D,VerticalBarChart): + _cmpZ=lambda self,a,b:cmp((-a[1],a[3],a[0],-a[4]),(-b[1],b[3],b[0],-b[4])) + +class HorizontalBarChart3D(BarChart3D,HorizontalBarChart): + _cmpZ = lambda self,a,b: cmp((-a[1],a[4],a[0],-a[3]),(-b[1],b[4],b[0],-b[3])) #t, z0, z1, x, y = a[:5] + +# Vertical samples. +def sampleV0a(): + "A slightly pathologic bar chart with only TWO data items." + + drawing = Drawing(400, 200) + + data = [(13, 20)] + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'ne' + bc.categoryAxis.labels.dx = 8 + bc.categoryAxis.labels.dy = -2 + bc.categoryAxis.labels.angle = 30 + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleV0b(): + "A pathologic bar chart with only ONE data item." + + drawing = Drawing(400, 200) + + data = [(42,)] + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 50 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'ne' + bc.categoryAxis.labels.dx = 8 + bc.categoryAxis.labels.dy = -2 + bc.categoryAxis.labels.angle = 30 + bc.categoryAxis.categoryNames = ['Jan-99'] + + drawing.add(bc) + + return drawing + + +def sampleV0c(): + "A really pathologic bar chart with NO data items at all!" + + drawing = Drawing(400, 200) + + data = [()] + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'ne' + bc.categoryAxis.labels.dx = 8 + bc.categoryAxis.labels.dy = -2 + bc.categoryAxis.categoryNames = [] + + drawing.add(bc) + + return drawing + + +def sampleV1(): + "Sample of multi-series bar chart." + + drawing = Drawing(400, 200) + + data = [ + (13, 5, 20, 22, 37, 45, 19, 4), + (14, 6, 21, 23, 38, 46, 20, 5) + ] + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'ne' + bc.categoryAxis.labels.dx = 8 + bc.categoryAxis.labels.dy = -2 + bc.categoryAxis.labels.angle = 30 + + catNames = string.split('Jan Feb Mar Apr May Jun Jul Aug', ' ') + catNames = map(lambda n:n+'-99', catNames) + bc.categoryAxis.categoryNames = catNames + drawing.add(bc) + + return drawing + + +def sampleV2a(): + "Sample of multi-series bar chart." + + data = [(2.4, -5.7, 2, 5, 9.2), + (0.6, -4.9, -3, 4, 6.8) + ] + + labels = ("Q3 2000", "Year to Date", "12 months", + "Annualised\n3 years", "Since 07.10.99") + + drawing = Drawing(400, 200) + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 120 + bc.width = 300 + bc.data = data + + bc.barSpacing = 0 + bc.groupSpacing = 10 + bc.barWidth = 10 + + bc.valueAxis.valueMin = -15 + bc.valueAxis.valueMax = +15 + bc.valueAxis.valueStep = 5 + bc.valueAxis.labels.fontName = 'Helvetica' + bc.valueAxis.labels.fontSize = 8 + bc.valueAxis.labels.boxAnchor = 'n' # irrelevant (becomes 'c') + bc.valueAxis.labels.textAnchor = 'middle' + + bc.categoryAxis.categoryNames = labels + bc.categoryAxis.labels.fontName = 'Helvetica' + bc.categoryAxis.labels.fontSize = 8 + bc.categoryAxis.labels.dy = -60 + + drawing.add(bc) + + return drawing + + +def sampleV2b(): + "Sample of multi-series bar chart." + + data = [(2.4, -5.7, 2, 5, 9.2), + (0.6, -4.9, -3, 4, 6.8) + ] + + labels = ("Q3 2000", "Year to Date", "12 months", + "Annualised\n3 years", "Since 07.10.99") + + drawing = Drawing(400, 200) + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 120 + bc.width = 300 + bc.data = data + + bc.barSpacing = 5 + bc.groupSpacing = 10 + bc.barWidth = 10 + + bc.valueAxis.valueMin = -15 + bc.valueAxis.valueMax = +15 + bc.valueAxis.valueStep = 5 + bc.valueAxis.labels.fontName = 'Helvetica' + bc.valueAxis.labels.fontSize = 8 + bc.valueAxis.labels.boxAnchor = 'n' # irrelevant (becomes 'c') + bc.valueAxis.labels.textAnchor = 'middle' + + bc.categoryAxis.categoryNames = labels + bc.categoryAxis.labels.fontName = 'Helvetica' + bc.categoryAxis.labels.fontSize = 8 + bc.categoryAxis.labels.dy = -60 + + drawing.add(bc) + + return drawing + + +def sampleV2c(): + "Sample of multi-series bar chart." + + data = [(2.4, -5.7, 2, 5, 9.99), + (0.6, -4.9, -3, 4, 9.99) + ] + + labels = ("Q3 2000", "Year to Date", "12 months", + "Annualised\n3 years", "Since 07.10.99") + + drawing = Drawing(400, 200) + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 120 + bc.width = 300 + bc.data = data + + bc.barSpacing = 2 + bc.groupSpacing = 10 + bc.barWidth = 10 + + bc.valueAxis.valueMin = -15 + bc.valueAxis.valueMax = +15 + bc.valueAxis.valueStep = 5 + bc.valueAxis.labels.fontName = 'Helvetica' + bc.valueAxis.labels.fontSize = 8 + + bc.categoryAxis.categoryNames = labels + bc.categoryAxis.labels.fontName = 'Helvetica' + bc.categoryAxis.labels.fontSize = 8 + bc.valueAxis.labels.boxAnchor = 'n' + bc.valueAxis.labels.textAnchor = 'middle' + bc.categoryAxis.labels.dy = -60 + + bc.barLabels.nudge = 10 + + bc.barLabelFormat = '%0.2f' + bc.barLabels.dx = 0 + bc.barLabels.dy = 0 + bc.barLabels.boxAnchor = 'n' # irrelevant (becomes 'c') + bc.barLabels.fontName = 'Helvetica' + bc.barLabels.fontSize = 6 + + drawing.add(bc) + + return drawing + + +def sampleV3(): + "Faked horizontal bar chart using a vertical real one (deprecated)." + + names = ("UK Equities", "US Equities", "European Equities", "Japanese Equities", + "Pacific (ex Japan) Equities", "Emerging Markets Equities", + "UK Bonds", "Overseas Bonds", "UK Index-Linked", "Cash") + + series1 = (-1.5, 0.3, 0.5, 1.0, 0.8, 0.7, 0.4, 0.1, 1.0, 0.3) + series2 = (0.0, 0.33, 0.55, 1.1, 0.88, 0.77, 0.44, 0.11, 1.10, 0.33) + + assert len(names) == len(series1), "bad data" + assert len(names) == len(series2), "bad data" + + drawing = Drawing(400, 200) + + bc = VerticalBarChart() + bc.x = 0 + bc.y = 0 + bc.height = 100 + bc.width = 150 + bc.data = (series1,) + bc.bars.fillColor = colors.green + + bc.barLabelFormat = '%0.2f' + bc.barLabels.dx = 0 + bc.barLabels.dy = 0 + bc.barLabels.boxAnchor = 'w' # irrelevant (becomes 'c') + bc.barLabels.angle = 90 + bc.barLabels.fontName = 'Helvetica' + bc.barLabels.fontSize = 6 + bc.barLabels.nudge = 10 + + bc.valueAxis.visible = 0 + bc.valueAxis.valueMin = -2 + bc.valueAxis.valueMax = +2 + bc.valueAxis.valueStep = 1 + + bc.categoryAxis.tickUp = 0 + bc.categoryAxis.tickDown = 0 + bc.categoryAxis.categoryNames = names + bc.categoryAxis.labels.angle = 90 + bc.categoryAxis.labels.boxAnchor = 'w' + bc.categoryAxis.labels.dx = 0 + bc.categoryAxis.labels.dy = -125 + bc.categoryAxis.labels.fontName = 'Helvetica' + bc.categoryAxis.labels.fontSize = 6 + + g = Group(bc) + g.translate(100, 175) + g.rotate(-90) + + drawing.add(g) + + return drawing + + +def sampleV4a(): + "A bar chart showing value axis region starting at *exactly* zero." + + drawing = Drawing(400, 200) + + data = [(13, 20)] + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'n' + bc.categoryAxis.labels.dy = -5 + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleV4b(): + "A bar chart showing value axis region starting *below* zero." + + drawing = Drawing(400, 200) + + data = [(13, 20)] + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = -10 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'n' + bc.categoryAxis.labels.dy = -5 + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleV4c(): + "A bar chart showing value axis region staring *above* zero." + + drawing = Drawing(400, 200) + + data = [(13, 20)] + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = 10 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'n' + bc.categoryAxis.labels.dy = -5 + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleV4d(): + "A bar chart showing value axis region entirely *below* zero." + + drawing = Drawing(400, 200) + + data = [(-13, -20)] + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = -30 + bc.valueAxis.valueMax = -10 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'n' + bc.categoryAxis.labels.dy = -5 + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +### + +##dataSample5 = [(10, 20), (20, 30), (30, 40), (40, 50), (50, 60)] +##dataSample5 = [(10, 60), (20, 50), (30, 40), (40, 30), (50, 20)] +dataSample5 = [(10, 60), (20, 50), (30, 40), (40, 30)] + +def sampleV5a(): + "A simple bar chart with no expressed spacing attributes." + + drawing = Drawing(400, 200) + + data = dataSample5 + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'n' + bc.categoryAxis.labels.dy = -5 + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleV5b(): + "A simple bar chart with proportional spacing." + + drawing = Drawing(400, 200) + + data = dataSample5 + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.useAbsolute = 0 + bc.barWidth = 40 + bc.groupSpacing = 20 + bc.barSpacing = 10 + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'n' + bc.categoryAxis.labels.dy = -5 + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleV5c1(): + "Make sampe simple bar chart but with absolute spacing." + + drawing = Drawing(400, 200) + + data = dataSample5 + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.useAbsolute = 1 + bc.barWidth = 40 + bc.groupSpacing = 0 + bc.barSpacing = 0 + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'n' + bc.categoryAxis.labels.dy = -5 + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleV5c2(): + "Make sampe simple bar chart but with absolute spacing." + + drawing = Drawing(400, 200) + + data = dataSample5 + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.useAbsolute = 1 + bc.barWidth = 40 + bc.groupSpacing = 20 + bc.barSpacing = 0 + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'n' + bc.categoryAxis.labels.dy = -5 + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleV5c3(): + "Make sampe simple bar chart but with absolute spacing." + + drawing = Drawing(400, 200) + + data = dataSample5 + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.useAbsolute = 1 + bc.barWidth = 40 + bc.groupSpacing = 0 + bc.barSpacing = 10 + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'n' + bc.categoryAxis.labels.dy = -5 + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleV5c4(): + "Make sampe simple bar chart but with absolute spacing." + + drawing = Drawing(400, 200) + + data = dataSample5 + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.useAbsolute = 1 + bc.barWidth = 40 + bc.groupSpacing = 20 + bc.barSpacing = 10 + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'n' + bc.categoryAxis.labels.dy = -5 + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +# Horizontal samples + +def sampleH0a(): + "Make a slightly pathologic bar chart with only TWO data items." + + drawing = Drawing(400, 200) + + data = [(13, 20)] + + bc = HorizontalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'se' + bc.categoryAxis.labels.angle = 30 + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleH0b(): + "Make a pathologic bar chart with only ONE data item." + + drawing = Drawing(400, 200) + + data = [(42,)] + + bc = HorizontalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 50 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'se' + bc.categoryAxis.labels.angle = 30 + bc.categoryAxis.categoryNames = ['Jan-99'] + + drawing.add(bc) + + return drawing + + +def sampleH0c(): + "Make a really pathologic bar chart with NO data items at all!" + + drawing = Drawing(400, 200) + + data = [()] + + bc = HorizontalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'se' + bc.categoryAxis.labels.angle = 30 + bc.categoryAxis.categoryNames = [] + + drawing.add(bc) + + return drawing + + +def sampleH1(): + "Sample of multi-series bar chart." + + drawing = Drawing(400, 200) + + data = [ + (13, 5, 20, 22, 37, 45, 19, 4), + (14, 6, 21, 23, 38, 46, 20, 5) + ] + + bc = HorizontalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'e' + catNames = string.split('Jan Feb Mar Apr May Jun Jul Aug', ' ') + catNames = map(lambda n:n+'-99', catNames) + bc.categoryAxis.categoryNames = catNames + drawing.add(bc, 'barchart') + + return drawing + + +def sampleH2a(): + "Sample of multi-series bar chart." + + data = [(2.4, -5.7, 2, 5, 9.2), + (0.6, -4.9, -3, 4, 6.8) + ] + + labels = ("Q3 2000", "Year to Date", "12 months", + "Annualised\n3 years", "Since 07.10.99") + + drawing = Drawing(400, 200) + + bc = HorizontalBarChart() + bc.x = 80 + bc.y = 50 + bc.height = 120 + bc.width = 300 + bc.data = data + + bc.barSpacing = 0 + bc.groupSpacing = 10 + bc.barWidth = 10 + + bc.valueAxis.valueMin = -15 + bc.valueAxis.valueMax = +15 + bc.valueAxis.valueStep = 5 + bc.valueAxis.labels.fontName = 'Helvetica' + bc.valueAxis.labels.fontSize = 8 + bc.valueAxis.labels.boxAnchor = 'n' # irrelevant (becomes 'c') + bc.valueAxis.labels.textAnchor = 'middle' + bc.valueAxis.configure(bc.data) + + bc.categoryAxis.categoryNames = labels + bc.categoryAxis.labels.fontName = 'Helvetica' + bc.categoryAxis.labels.fontSize = 8 + bc.categoryAxis.labels.dx = -150 + + drawing.add(bc) + + return drawing + + +def sampleH2b(): + "Sample of multi-series bar chart." + + data = [(2.4, -5.7, 2, 5, 9.2), + (0.6, -4.9, -3, 4, 6.8) + ] + + labels = ("Q3 2000", "Year to Date", "12 months", + "Annualised\n3 years", "Since 07.10.99") + + drawing = Drawing(400, 200) + + bc = HorizontalBarChart() + bc.x = 80 + bc.y = 50 + bc.height = 120 + bc.width = 300 + bc.data = data + + bc.barSpacing = 5 + bc.groupSpacing = 10 + bc.barWidth = 10 + + bc.valueAxis.valueMin = -15 + bc.valueAxis.valueMax = +15 + bc.valueAxis.valueStep = 5 + bc.valueAxis.labels.fontName = 'Helvetica' + bc.valueAxis.labels.fontSize = 8 + bc.valueAxis.labels.boxAnchor = 'n' # irrelevant (becomes 'c') + bc.valueAxis.labels.textAnchor = 'middle' + + bc.categoryAxis.categoryNames = labels + bc.categoryAxis.labels.fontName = 'Helvetica' + bc.categoryAxis.labels.fontSize = 8 + bc.categoryAxis.labels.dx = -150 + + drawing.add(bc) + + return drawing + + +def sampleH2c(): + "Sample of multi-series bar chart." + + data = [(2.4, -5.7, 2, 5, 9.99), + (0.6, -4.9, -3, 4, 9.99) + ] + + labels = ("Q3 2000", "Year to Date", "12 months", + "Annualised\n3 years", "Since 07.10.99") + + drawing = Drawing(400, 200) + + bc = HorizontalBarChart() + bc.x = 80 + bc.y = 50 + bc.height = 120 + bc.width = 300 + bc.data = data + + bc.barSpacing = 2 + bc.groupSpacing = 10 + bc.barWidth = 10 + + bc.valueAxis.valueMin = -15 + bc.valueAxis.valueMax = +15 + bc.valueAxis.valueStep = 5 + bc.valueAxis.labels.fontName = 'Helvetica' + bc.valueAxis.labels.fontSize = 8 + bc.valueAxis.labels.boxAnchor = 'n' + bc.valueAxis.labels.textAnchor = 'middle' + + bc.categoryAxis.categoryNames = labels + bc.categoryAxis.labels.fontName = 'Helvetica' + bc.categoryAxis.labels.fontSize = 8 + bc.categoryAxis.labels.dx = -150 + + bc.barLabels.nudge = 10 + + bc.barLabelFormat = '%0.2f' + bc.barLabels.dx = 0 + bc.barLabels.dy = 0 + bc.barLabels.boxAnchor = 'n' # irrelevant (becomes 'c') + bc.barLabels.fontName = 'Helvetica' + bc.barLabels.fontSize = 6 + + drawing.add(bc) + + return drawing + + +def sampleH3(): + "A really horizontal bar chart (compared to the equivalent faked one)." + + names = ("UK Equities", "US Equities", "European Equities", "Japanese Equities", + "Pacific (ex Japan) Equities", "Emerging Markets Equities", + "UK Bonds", "Overseas Bonds", "UK Index-Linked", "Cash") + + series1 = (-1.5, 0.3, 0.5, 1.0, 0.8, 0.7, 0.4, 0.1, 1.0, 0.3) + series2 = (0.0, 0.33, 0.55, 1.1, 0.88, 0.77, 0.44, 0.11, 1.10, 0.33) + + assert len(names) == len(series1), "bad data" + assert len(names) == len(series2), "bad data" + + drawing = Drawing(400, 200) + + bc = HorizontalBarChart() + bc.x = 100 + bc.y = 20 + bc.height = 150 + bc.width = 250 + bc.data = (series1,) + bc.bars.fillColor = colors.green + + bc.barLabelFormat = '%0.2f' + bc.barLabels.dx = 0 + bc.barLabels.dy = 0 + bc.barLabels.boxAnchor = 'w' # irrelevant (becomes 'c') + bc.barLabels.fontName = 'Helvetica' + bc.barLabels.fontSize = 6 + bc.barLabels.nudge = 10 + + bc.valueAxis.visible = 0 + bc.valueAxis.valueMin = -2 + bc.valueAxis.valueMax = +2 + bc.valueAxis.valueStep = 1 + + bc.categoryAxis.tickLeft = 0 + bc.categoryAxis.tickRight = 0 + bc.categoryAxis.categoryNames = names + bc.categoryAxis.labels.boxAnchor = 'w' + bc.categoryAxis.labels.dx = -170 + bc.categoryAxis.labels.fontName = 'Helvetica' + bc.categoryAxis.labels.fontSize = 6 + + g = Group(bc) + drawing.add(g) + + return drawing + + +def sampleH4a(): + "A bar chart showing value axis region starting at *exactly* zero." + + drawing = Drawing(400, 200) + + data = [(13, 20)] + + bc = HorizontalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'e' + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleH4b(): + "A bar chart showing value axis region starting *below* zero." + + drawing = Drawing(400, 200) + + data = [(13, 20)] + + bc = HorizontalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = -10 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'e' + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleH4c(): + "A bar chart showing value axis region starting *above* zero." + + drawing = Drawing(400, 200) + + data = [(13, 20)] + + bc = HorizontalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = 10 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'e' + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleH4d(): + "A bar chart showing value axis region entirely *below* zero." + + drawing = Drawing(400, 200) + + data = [(-13, -20)] + + bc = HorizontalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = -30 + bc.valueAxis.valueMax = -10 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'e' + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +dataSample5 = [(10, 60), (20, 50), (30, 40), (40, 30)] + +def sampleH5a(): + "A simple bar chart with no expressed spacing attributes." + + drawing = Drawing(400, 200) + + data = dataSample5 + + bc = HorizontalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'e' + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleH5b(): + "A simple bar chart with proportional spacing." + + drawing = Drawing(400, 200) + + data = dataSample5 + + bc = HorizontalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.useAbsolute = 0 + bc.barWidth = 40 + bc.groupSpacing = 20 + bc.barSpacing = 10 + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'e' + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleH5c1(): + "A simple bar chart with absolute spacing." + + drawing = Drawing(400, 200) + + data = dataSample5 + + bc = HorizontalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.useAbsolute = 1 + bc.barWidth = 10 + bc.groupSpacing = 0 + bc.barSpacing = 0 + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'e' + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleH5c2(): + "Simple bar chart with absolute spacing." + + drawing = Drawing(400, 200) + + data = dataSample5 + + bc = HorizontalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.useAbsolute = 1 + bc.barWidth = 10 + bc.groupSpacing = 20 + bc.barSpacing = 0 + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'e' + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleH5c3(): + "Simple bar chart with absolute spacing." + + drawing = Drawing(400, 200) + + data = dataSample5 + + bc = HorizontalBarChart() + bc.x = 50 + bc.y = 20 + bc.height = 155 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.useAbsolute = 1 + bc.barWidth = 10 + bc.groupSpacing = 0 + bc.barSpacing = 2 + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'e' + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleH5c4(): + "Simple bar chart with absolute spacing." + + drawing = Drawing(400, 200) + + data = dataSample5 + + bc = HorizontalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.useAbsolute = 1 + bc.barWidth = 10 + bc.groupSpacing = 20 + bc.barSpacing = 10 + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'e' + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + +def sampleSymbol1(): + "Simple bar chart using symbol attribute." + + drawing = Drawing(400, 200) + + data = dataSample5 + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.barWidth = 10 + bc.groupSpacing = 15 + bc.barSpacing = 3 + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'e' + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + from reportlab.graphics.widgets.grids import ShadedRect + sym1 = ShadedRect() + sym1.fillColorStart = colors.black + sym1.fillColorEnd = colors.blue + sym1.orientation = 'horizontal' + sym1.strokeWidth = 0 + + sym2 = ShadedRect() + sym2.fillColorStart = colors.black + sym2.fillColorEnd = colors.pink + sym2.orientation = 'horizontal' + sym2.strokeWidth = 0 + + sym3 = ShadedRect() + sym3.fillColorStart = colors.blue + sym3.fillColorEnd = colors.white + sym3.orientation = 'vertical' + sym3.cylinderMode = 1 + sym3.strokeWidth = 0 + + bc.bars.symbol = sym1 + bc.bars[2].symbol = sym2 + bc.bars[3].symbol = sym3 + + drawing.add(bc) + + return drawing + +def sampleStacked1(): + "Simple bar chart using symbol attribute." + + drawing = Drawing(400, 200) + + data = dataSample5 + + bc = VerticalBarChart() + bc.categoryAxis.style = 'stacked' + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.barWidth = 10 + bc.groupSpacing = 15 + bc.valueAxis.valueMin = 0 + + bc.categoryAxis.labels.boxAnchor = 'e' + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + from reportlab.graphics.widgets.grids import ShadedRect + bc.bars.symbol = ShadedRect() + bc.bars.symbol.fillColorStart = colors.red + bc.bars.symbol.fillColorEnd = colors.white + bc.bars.symbol.orientation = 'vertical' + bc.bars.symbol.cylinderMode = 1 + bc.bars.symbol.strokeWidth = 0 + + bc.bars[1].symbol = ShadedRect() + bc.bars[1].symbol.fillColorStart = colors.magenta + bc.bars[1].symbol.fillColorEnd = colors.white + bc.bars[1].symbol.orientation = 'vertical' + bc.bars[1].symbol.cylinderMode = 1 + bc.bars[1].symbol.strokeWidth = 0 + + bc.bars[2].symbol = ShadedRect() + bc.bars[2].symbol.fillColorStart = colors.green + bc.bars[2].symbol.fillColorEnd = colors.white + bc.bars[2].symbol.orientation = 'vertical' + bc.bars[2].symbol.cylinderMode = 1 + bc.bars[2].symbol.strokeWidth = 0 + + bc.bars[3].symbol = ShadedRect() + bc.bars[3].symbol.fillColorStart = colors.blue + bc.bars[3].symbol.fillColorEnd = colors.white + bc.bars[3].symbol.orientation = 'vertical' + bc.bars[3].symbol.cylinderMode = 1 + bc.bars[3].symbol.strokeWidth = 0 + + drawing.add(bc) + + return drawing + +#class version of function sampleH5c4 above +class SampleH5c4(Drawing): + "Simple bar chart with absolute spacing." + + def __init__(self,width=400,height=200,*args,**kw): + apply(Drawing.__init__,(self,width,height)+args,kw) + bc = HorizontalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = dataSample5 + bc.strokeColor = colors.black + + bc.useAbsolute = 1 + bc.barWidth = 10 + bc.groupSpacing = 20 + bc.barSpacing = 10 + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'e' + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + self.add(bc,name='HBC') diff --git a/bin/reportlab/graphics/charts/dotbox.py b/bin/reportlab/graphics/charts/dotbox.py new file mode 100644 index 00000000000..6962d278822 --- /dev/null +++ b/bin/reportlab/graphics/charts/dotbox.py @@ -0,0 +1,165 @@ +from reportlab.lib.colors import blue, _PCMYK_black +from reportlab.graphics.charts.textlabels import Label +from reportlab.graphics.shapes import Circle, Drawing, Group, Line, Rect, String +from reportlab.graphics.widgetbase import Widget +from reportlab.lib.attrmap import * +from reportlab.lib.validators import * +from reportlab.lib.units import cm +from reportlab.pdfbase.pdfmetrics import getFont +from reportlab.graphics.charts.lineplots import _maxWidth + +class DotBox(Widget): + """Returns a dotbox widget.""" + + #Doesn't use TypedPropertyCollection for labels - this can be a later improvement + _attrMap = AttrMap( + xlabels = AttrMapValue(isNoneOrListOfNoneOrStrings, + desc="List of text labels for boxes on left hand side"), + ylabels = AttrMapValue(isNoneOrListOfNoneOrStrings, + desc="Text label for second box on left hand side"), + labelFontName = AttrMapValue(isString, + desc="Name of font used for the labels"), + labelFontSize = AttrMapValue(isNumber, + desc="Size of font used for the labels"), + labelOffset = AttrMapValue(isNumber, + desc="Space between label text and grid edge"), + strokeWidth = AttrMapValue(isNumber, + desc='Width of the grid and dot outline'), + gridDivWidth = AttrMapValue(isNumber, + desc="Width of each 'box'"), + gridColor = AttrMapValue(isColor, + desc='Colour for the box and gridding'), + dotDiameter = AttrMapValue(isNumber, + desc="Diameter of the circle used for the 'dot'"), + dotColor = AttrMapValue(isColor, + desc='Colour of the circle on the box'), + dotXPosition = AttrMapValue(isNumber, + desc='X Position of the circle'), + dotYPosition = AttrMapValue(isNumber, + desc='X Position of the circle'), + x = AttrMapValue(isNumber, + desc='X Position of dotbox'), + y = AttrMapValue(isNumber, + desc='Y Position of dotbox'), + ) + + def __init__(self): + self.xlabels=["Value", "Blend", "Growth"] + self.ylabels=["Small", "Medium", "Large"] + self.labelFontName = "Helvetica" + self.labelFontSize = 6 + self.labelOffset = 5 + self.strokeWidth = 0.5 + self.gridDivWidth=0.5*cm + self.gridColor=colors.Color(25/255.0,77/255.0,135/255.0) + self.dotDiameter=0.4*cm + self.dotColor=colors.Color(232/255.0,224/255.0,119/255.0) + self.dotXPosition = 1 + self.dotYPosition = 1 + self.x = 30 + self.y = 5 + + + def _getDrawingDimensions(self): + leftPadding=rightPadding=topPadding=bottomPadding=5 + #find width of grid + tx=len(self.xlabels)*self.gridDivWidth + #add padding (and offset) + tx=tx+leftPadding+rightPadding+self.labelOffset + #add in maximum width of text + tx=tx+_maxWidth(self.xlabels, self.labelFontName, self.labelFontSize) + #find height of grid + ty=len(self.ylabels)*self.gridDivWidth + #add padding (and offset) + ty=ty+topPadding+bottomPadding+self.labelOffset + #add in maximum width of text + ty=ty+_maxWidth(self.ylabels, self.labelFontName, self.labelFontSize) + #print (tx, ty) + return (tx,ty) + + def demo(self,drawing=None): + if not drawing: + tx,ty=self._getDrawingDimensions() + drawing = Drawing(tx,ty) + drawing.add(self.draw()) + return drawing + + def draw(self): + g = Group() + + #box + g.add(Rect(self.x,self.y,len(self.xlabels)*self.gridDivWidth,len(self.ylabels)*self.gridDivWidth, + strokeColor=self.gridColor, + strokeWidth=self.strokeWidth, + fillColor=None)) + + #internal gridding + for f in range (1,len(self.ylabels)): + #horizontal + g.add(Line(strokeColor=self.gridColor, + strokeWidth=self.strokeWidth, + x1 = self.x, + y1 = self.y+f*self.gridDivWidth, + x2 = self.x+len(self.xlabels)*self.gridDivWidth, + y2 = self.y+f*self.gridDivWidth)) + for f in range (1,len(self.xlabels)): + #vertical + g.add(Line(strokeColor=self.gridColor, + strokeWidth=self.strokeWidth, + x1 = self.x+f*self.gridDivWidth, + y1 = self.y, + x2 = self.x+f*self.gridDivWidth, + y2 = self.y+len(self.ylabels)*self.gridDivWidth)) + + # draw the 'dot' + g.add(Circle(strokeColor=self.gridColor, + strokeWidth=self.strokeWidth, + fillColor=self.dotColor, + cx = self.x+(self.dotXPosition*self.gridDivWidth), + cy = self.y+(self.dotYPosition*self.gridDivWidth), + r = self.dotDiameter/2.0)) + + #used for centering y-labels (below) + ascent=getFont(self.labelFontName).face.ascent + if ascent==0: + ascent=0.718 # default (from helvetica) + ascent=ascent*self.labelFontSize # normalize + + #do y-labels + if self.ylabels != None: + for f in range (len(self.ylabels)-1,-1,-1): + if self.ylabels[f]!= None: + g.add(String(strokeColor=self.gridColor, + text = self.ylabels[f], + fontName = self.labelFontName, + fontSize = self.labelFontSize, + fillColor=_PCMYK_black, + x = self.x-self.labelOffset, + y = self.y+(f*self.gridDivWidth+(self.gridDivWidth-ascent)/2.0), + textAnchor = 'end')) + + #do x-labels + if self.xlabels != None: + for f in range (0,len(self.xlabels)): + if self.xlabels[f]!= None: + l=Label() + l.x=self.x+(f*self.gridDivWidth)+(self.gridDivWidth+ascent)/2.0 + l.y=self.y+(len(self.ylabels)*self.gridDivWidth)+self.labelOffset + l.angle=90 + l.textAnchor='start' + l.fontName = self.labelFontName + l.fontSize = self.labelFontSize + l.fillColor = _PCMYK_black + l.setText(self.xlabels[f]) + l.boxAnchor = 'sw' + l.draw() + g.add(l) + + return g + + + + +if __name__ == "__main__": + d = DotBox() + d.demo().save(fnRoot="dotbox")
\ No newline at end of file diff --git a/bin/reportlab/graphics/charts/doughnut.py b/bin/reportlab/graphics/charts/doughnut.py new file mode 100644 index 00000000000..f365f9a3fb8 --- /dev/null +++ b/bin/reportlab/graphics/charts/doughnut.py @@ -0,0 +1,349 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/doughnut.py +# doughnut chart + +"""Doughnut chart + +Produces a circular chart like the doughnut charts produced by Excel. +Can handle multiple series (which produce concentric 'rings' in the chart). + +""" +__version__=''' $Id: doughnut.py 2499 2004-12-29 17:12:34Z rgbecker $ ''' + +import copy +from math import sin, cos, pi +from types import ListType, TupleType +from reportlab.lib import colors +from reportlab.lib.validators import isColor, isNumber, isListOfNumbersOrNone,\ + isListOfNumbers, isColorOrNone, isString,\ + isListOfStringsOrNone, OneOf, SequenceOf,\ + isBoolean, isListOfColors,\ + isNoneOrListOfNoneOrStrings,\ + isNoneOrListOfNoneOrNumbers,\ + isNumberOrNone +from reportlab.lib.attrmap import * +from reportlab.pdfgen.canvas import Canvas +from reportlab.graphics.shapes import Group, Drawing, Line, Rect, Polygon, Ellipse, \ + Wedge, String, SolidShape, UserNode, STATE_DEFAULTS +from reportlab.graphics.widgetbase import Widget, TypedPropertyCollection, PropHolder +from reportlab.graphics.charts.piecharts import AbstractPieChart, WedgeProperties, _addWedgeLabel +from reportlab.graphics.charts.textlabels import Label +from reportlab.graphics.widgets.markers import Marker + +class SectorProperties(WedgeProperties): + """This holds descriptive information about the sectors in a doughnut chart. + + It is not to be confused with the 'sector itself'; this just holds + a recipe for how to format one, and does not allow you to hack the + angles. It can format a genuine Sector object for you with its + format method. + """ + _attrMap = AttrMap(BASE=WedgeProperties, + ) + +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 = 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() + sn = 0 + + 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 series in normData: + i = 0 + for angle in series: + endAngle = (startAngle + (angle * whichWay)) #% 360 + if abs(startAngle-endAngle)>=1e-5: + if startAngle < endAngle: + a1 = startAngle + a2 = endAngle + else: + a1 = endAngle + a2 = startAngle + + #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) + startAngle = endAngle + + 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) + _addWedgeLabel(self,text,g.add,averageAngle,labelX,labelY,sectorStyle) + i = i + 1 + sn = sn + 1 + + else: + i = 0 + #single series doughnut + iradius = self.height/5.0 + for angle in normData: + endAngle = (startAngle + (angle * whichWay)) #% 360 + if abs(startAngle-endAngle)>=1e-5: + if startAngle < endAngle: + a1 = startAngle + a2 = endAngle + else: + a1 = endAngle + a2 = startAngle + + #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) + + startAngle = endAngle + i = i + 1 + + return g + + def draw(self): + g = Group() + g.add(self.makeSectors()) + return g + + +def sample1(): + "Make up something from the individual Sectors" + + d = Drawing(400, 400) + g = Group() + + s1 = Wedge(centerx=200, centery=200, radius=150, startangledegrees=0, endangledegrees=120, radius1=100) + s1.fillColor=colors.red + s1.strokeColor=None + d.add(s1) + s2 = Wedge(centerx=200, centery=200, radius=150, startangledegrees=120, endangledegrees=240, radius1=100) + s2.fillColor=colors.green + s2.strokeColor=None + d.add(s2) + s3 = Wedge(centerx=200, centery=200, radius=150, startangledegrees=240, endangledegrees=260, radius1=100) + s3.fillColor=colors.blue + s3.strokeColor=None + d.add(s3) + s4 = Wedge(centerx=200, centery=200, radius=150, startangledegrees=260, endangledegrees=360, radius1=100) + s4.fillColor=colors.gray + s4.strokeColor=None + d.add(s4) + + return d + +def sample2(): + "Make a simple demo" + + d = Drawing(400, 400) + + dn = Doughnut() + dn.x = 50 + dn.y = 50 + dn.width = 300 + dn.height = 300 + dn.data = [10,20,30,40,50,60] + + d.add(dn) + + return d + +def sample3(): + "Make a more complex demo" + + d = Drawing(400, 400) + dn = Doughnut() + dn.x = 50 + dn.y = 50 + dn.width = 300 + dn.height = 300 + dn.data = [[10,20,30,40,50,60], [10,20,30,40]] + dn.labels = ['a','b','c','d','e','f'] + + d.add(dn) + + return d + +if __name__=='__main__': + + from reportlab.graphics.renderPDF import drawToFile + d = sample1() + drawToFile(d, 'doughnut1.pdf') + d = sample2() + drawToFile(d, 'doughnut2.pdf') + d = sample3() + drawToFile(d, 'doughnut3.pdf') diff --git a/bin/reportlab/graphics/charts/legends.py b/bin/reportlab/graphics/charts/legends.py new file mode 100644 index 00000000000..fae4d2b4fec --- /dev/null +++ b/bin/reportlab/graphics/charts/legends.py @@ -0,0 +1,611 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/legends.py +"""This will be a collection of legends to be used with charts. +""" +__version__=''' $Id: legends.py 2604 2005-06-08 10:12:46Z rgbecker $ ''' + +import copy, operator + +from reportlab.lib import colors +from reportlab.lib.validators import isNumber, OneOf, isString, isColorOrNone,\ + isNumberOrNone, isListOfNumbersOrNone, isStringOrNone, isBoolean,\ + NoneOr, AutoOr, isAuto, Auto, isBoxAnchor, SequenceOf +from reportlab.lib.attrmap import * +from reportlab.pdfbase.pdfmetrics import stringWidth, getFont +from reportlab.graphics.widgetbase import Widget, TypedPropertyCollection, PropHolder +from reportlab.graphics.shapes import Drawing, Group, String, Rect, Line, STATE_DEFAULTS +from reportlab.graphics.charts.areas import PlotArea +from reportlab.graphics.widgets.markers import uSymbol2Symbol, isSymbol +from reportlab.lib.utils import isSeqType + + +def _getStr(s): + if isSeqType(s): + return map(str,s) + else: + return str(s) + +def _getLines(s): + if isSeqType(s): + return tuple([(x or '').split('\n') for x in s]) + else: + return (s or '').split('\n') + +def _getLineCount(s): + T = _getLines(s) + if isSeqType(s): + return max([len(x) for x in T]) + else: + return len(T) + +def _getWidth(s,fontName, fontSize, sepSpace=0): + if isSeqType(s): + sum = 0 + for t in s: + m = [stringWidth(x, fontName, fontSize) for x in t.split('\n')] + sum += m and max(m) or 0 + sum += (len(s)-1)*sepSpace + return sum + m = [stringWidth(x, fontName, fontSize) for x in s.split('\n')] + return m and max(m) or 0 + +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 + baw = ba not in ('nw','w','sw','autox') + 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 + else: + deltax = max(maxWidth+xW,deltax) + width = 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 baw: + 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 TotalAnnotator: + def __init__(self, lText='Total', rText='0.0', fontName='Times-Roman', fontSize=10, + fillColor=colors.black, strokeWidth=0.5, strokeColor=colors.black, strokeDashArray=None, + dx=0, dy=0, dly=0, dlx=(0,0)): + self.lText = lText + self.rText = rText + self.fontName = fontName + self.fontSize = fontSize + self.fillColor = fillColor + self.dy = dy + self.dx = dx + self.dly = dly + self.dlx = dlx + self.strokeWidth = strokeWidth + self.strokeColor = strokeColor + self.strokeDashArray = strokeDashArray + + def __call__(self,legend, g, x, xt, y, width, lWidth): + from reportlab.graphics.shapes import String, Line + fontSize = self.fontSize + fontName = self.fontName + fillColor = self.fillColor + strokeColor = self.strokeColor + strokeWidth = self.strokeWidth + ascent=getFont(fontName).face.ascent/1000. + if ascent==0: ascent=0.718 # default (from helvetica) + ascent *= fontSize + leading = fontSize*1.2 + yt = y+self.dy-ascent*1.3 + if self.lText and fillColor: + g.add(String(xt,yt,self.lText, + fontName=fontName, + fontSize=fontSize, + fillColor=fillColor, + textAnchor = "start")) + if self.rText: + g.add(String(xt+width,yt,self.rText, + fontName=fontName, + fontSize=fontSize, + fillColor=fillColor, + textAnchor = "end")) + if strokeWidth and strokeColor: + yL = y+self.dly-leading + g.add(Line(x+self.dlx[0],yL,x+self.dlx[1]+lWidth,yL, + strokeColor=strokeColor, strokeWidth=strokeWidth, + strokeDashArray=self.strokeDashArray)) + +class LineSwatch(Widget): + """basically a Line with properties added so it can be used in a LineLegend""" + _attrMap = AttrMap( + x = AttrMapValue(isNumber, desc="x-coordinate for swatch line start point"), + y = AttrMapValue(isNumber, desc="y-coordinate for swatch line start point"), + width = AttrMapValue(isNumber, desc="length of swatch line"), + height = AttrMapValue(isNumber, desc="used for line strokeWidth"), + strokeColor = AttrMapValue(isColorOrNone, desc="color of swatch line"), + strokeDashArray = AttrMapValue(isListOfNumbersOrNone, desc="dash array for swatch line"), + ) + + def __init__(self): + from reportlab.lib.colors import red + from reportlab.graphics.shapes import Line + self.x = 0 + self.y = 0 + self.width = 20 + self.height = 1 + self.strokeColor = red + self.strokeDashArray = None + + def draw(self): + l = Line(self.x,self.y,self.x+self.width,self.y) + l.strokeColor = self.strokeColor + l.strokeDashArray = self.strokeDashArray + l.strokeWidth = self.height + return l + +class LineLegend(Legend): + """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. + """ + + def __init__(self): + Legend.__init__(self) + + # Size of swatch rectangle. + self.dx = 10 + self.dy = 2 + + def _defaultSwatch(self,x,thisy,dx,dy,fillColor,strokeWidth,strokeColor): + l = LineSwatch() + l.x = x + l.y = thisy + l.width = dx + l.height = dy + l.strokeColor = fillColor + return l + +def sample1c(): + "Make sample legend." + + d = Drawing(200, 100) + + legend = Legend() + legend.alignment = 'right' + 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 + + +def sample2c(): + "Make sample legend." + + d = Drawing(200, 100) + + legend = Legend() + legend.alignment = 'right' + legend.x = 20 + legend.y = 90 + legend.deltax = 60 + legend.dxTextSpace = 10 + legend.columnMaximum = 4 + 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 + +def sample3(): + "Make sample legend with line swatches." + + d = Drawing(200, 100) + + legend = LineLegend() + legend.alignment = 'right' + legend.x = 20 + legend.y = 90 + legend.deltax = 60 + legend.dxTextSpace = 10 + legend.columnMaximum = 4 + 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 + + +def sample3a(): + "Make sample legend with line swatches and dasharrays on the lines." + + d = Drawing(200, 100) + + legend = LineLegend() + legend.alignment = 'right' + legend.x = 20 + legend.y = 90 + legend.deltax = 60 + legend.dxTextSpace = 10 + legend.columnMaximum = 4 + items = 'red green blue yellow pink black white'.split() + darrays = ([2,1], [2,5], [2,2,5,5], [1,2,3,4], [4,2,3,4], [1,2,3,4,5,6], [1]) + cnp = [] + for i in range(0, len(items)): + l = LineSwatch() + l.strokeColor = getattr(colors, items[i]) + l.strokeDashArray = darrays[i] + cnp.append((l, items[i])) + legend.colorNamePairs = cnp + d.add(legend, 'legend') + + return d diff --git a/bin/reportlab/graphics/charts/linecharts.py b/bin/reportlab/graphics/charts/linecharts.py new file mode 100644 index 00000000000..99ef982a644 --- /dev/null +++ b/bin/reportlab/graphics/charts/linecharts.py @@ -0,0 +1,695 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/linecharts.py +""" +This modules defines a very preliminary Line Chart example. +""" +__version__=''' $Id: linecharts.py 2493 2004-12-22 16:14:25Z rgbecker $ ''' + +import string +from types import FunctionType, StringType + +from reportlab.lib import colors +from reportlab.lib.validators import isNumber, isColor, isColorOrNone, isListOfStrings, \ + isListOfStringsOrNone, SequenceOf, isBoolean, NoneOr, \ + isListOfNumbersOrNone, isStringOrNone +from reportlab.lib.attrmap import * +from reportlab.lib.formatters import Formatter +from reportlab.graphics.widgetbase import Widget, TypedPropertyCollection, PropHolder +from reportlab.graphics.shapes import Line, Rect, Group, Drawing, Polygon, PolyLine +from reportlab.graphics.widgets.signsandsymbols import NoEntry +from reportlab.graphics.charts.axes import XCategoryAxis, YValueAxis +from reportlab.graphics.charts.textlabels import Label +from reportlab.graphics.widgets.markers import uSymbol2Symbol, isSymbol, makeMarker +from reportlab.graphics.charts.areas import PlotArea + +class LineChartProperties(PropHolder): + _attrMap = AttrMap( + strokeWidth = AttrMapValue(isNumber, desc='Width of a line.'), + strokeColor = AttrMapValue(isColorOrNone, desc='Color of a line.'), + strokeDashArray = AttrMapValue(isListOfNumbersOrNone, desc='Dash array of a line.'), + symbol = AttrMapValue(NoneOr(isSymbol), desc='Widget placed at data points.'), + shader = AttrMapValue(None, desc='Shader Class.'), + filler = AttrMapValue(None, desc='Filler Class.'), + name = AttrMapValue(isStringOrNone, desc='Name of the line.'), + ) + +class AbstractLineChart(PlotArea): + + def makeSwatchSample(self,rowNo, x, y, width, height): + baseStyle = self.lines + styleIdx = rowNo % len(baseStyle) + style = baseStyle[styleIdx] + color = style.strokeColor + y = y+height/2. + if self.joinedLines: + dash = getattr(style, 'strokeDashArray', getattr(baseStyle,'strokeDashArray',None)) + strokeWidth= getattr(style, 'strokeWidth', getattr(style, 'strokeWidth',None)) + L = Line(x,y,x+width,y,strokeColor=color,strokeLineCap=0) + if strokeWidth: L.strokeWidth = strokeWidth + if dash: L.strokeDashArray = dash + else: + L = None + + if hasattr(style, 'symbol'): + S = style.symbol + elif hasattr(baseStyle, 'symbol'): + S = baseStyle.symbol + else: + S = None + + if S: S = uSymbol2Symbol(S,x+width/2.,y,color) + if S and L: + g = Group() + g.add(L) + g.add(S) + return g + return S or L + + def getSeriesName(self,i,default=None): + '''return series name i or default''' + return getattr(self.lines[i],'name',default) + +class LineChart(AbstractLineChart): + pass + +# This is conceptually similar to the VerticalBarChart. +# Still it is better named HorizontalLineChart... :-/ + +class HorizontalLineChart(LineChart): + """Line chart with multiple lines. + + A line chart is assumed to have one category and one value axis. + Despite its generic name this particular line chart class has + a vertical value axis and a horizontal category one. It may + evolve into individual horizontal and vertical variants (like + with the existing bar charts). + + Available attributes are: + + x: x-position of lower-left chart origin + y: y-position of lower-left chart origin + width: chart width + height: chart height + + useAbsolute: disables auto-scaling of chart elements (?) + lineLabelNudge: distance of data labels to data points + lineLabels: labels associated with data values + lineLabelFormat: format string or callback function + groupSpacing: space between categories + + joinedLines: enables drawing of lines + + strokeColor: color of chart lines (?) + fillColor: color for chart background (?) + lines: style list, used cyclically for data series + + valueAxis: value axis object + categoryAxis: category axis object + categoryNames: category names + + data: chart data, a list of data series of equal length + """ + + _attrMap = AttrMap(BASE=LineChart, + useAbsolute = AttrMapValue(isNumber, desc='Flag to use absolute spacing values.'), + lineLabelNudge = AttrMapValue(isNumber, desc='Distance between a data point and its label.'), + lineLabels = AttrMapValue(None, desc='Handle to the list of data point labels.'), + lineLabelFormat = AttrMapValue(None, desc='Formatting string or function used for data point labels.'), + lineLabelArray = AttrMapValue(None, desc='explicit array of line label values, must match size of data if present.'), + groupSpacing = AttrMapValue(isNumber, desc='? - Likely to disappear.'), + joinedLines = AttrMapValue(isNumber, desc='Display data points joined with lines if true.'), + lines = AttrMapValue(None, desc='Handle of the lines.'), + valueAxis = AttrMapValue(None, desc='Handle of the value axis.'), + categoryAxis = AttrMapValue(None, desc='Handle of the category axis.'), + categoryNames = AttrMapValue(isListOfStringsOrNone, desc='List of category names.'), + data = AttrMapValue(None, desc='Data to be plotted, list of (lists of) numbers.'), + inFill = AttrMapValue(isBoolean, desc='Whether infilling should be done.'), + reversePlotOrder = AttrMapValue(isBoolean, desc='If true reverse plot order.'), + annotations = AttrMapValue(None, desc='list of callables, will be called with self, xscale, yscale.'), + ) + + def __init__(self): + LineChart.__init__(self) + + # Allow for a bounding rectangle. + self.strokeColor = None + self.fillColor = None + + # Named so we have less recoding for the horizontal one :-) + self.categoryAxis = XCategoryAxis() + self.valueAxis = YValueAxis() + + # This defines two series of 3 points. Just an example. + self.data = [(100,110,120,130), + (70, 80, 80, 90)] + self.categoryNames = ('North','South','East','West') + + self.lines = TypedPropertyCollection(LineChartProperties) + self.lines.strokeWidth = 1 + self.lines[0].strokeColor = colors.red + self.lines[1].strokeColor = colors.green + self.lines[2].strokeColor = colors.blue + + # control spacing. if useAbsolute = 1 then + # the next parameters are in points; otherwise + # they are 'proportions' and are normalized to + # fit the available space. + self.useAbsolute = 0 #- not done yet + self.groupSpacing = 1 #5 + + self.lineLabels = TypedPropertyCollection(Label) + self.lineLabelFormat = None + self.lineLabelArray = None + + # This says whether the origin is above or below + # the data point. +10 means put the origin ten points + # above the data point if value > 0, or ten + # points below if data value < 0. This is different + # to label dx/dy which are not dependent on the + # sign of the data. + self.lineLabelNudge = 10 + # If you have multiple series, by default they butt + # together. + + # New line chart attributes. + self.joinedLines = 1 # Connect items with straight lines. + self.inFill = 0 + self.reversePlotOrder = 0 + + + def demo(self): + """Shows basic use of a line chart.""" + + drawing = Drawing(200, 100) + + data = [ + (13, 5, 20, 22, 37, 45, 19, 4), + (14, 10, 21, 28, 38, 46, 25, 5) + ] + + lc = HorizontalLineChart() + + lc.x = 20 + lc.y = 10 + lc.height = 85 + lc.width = 170 + lc.data = data + lc.lines.symbol = makeMarker('Circle') + + drawing.add(lc) + + return drawing + + + def calcPositions(self): + """Works out where they go. + + Sets an attribute _positions which is a list of + lists of (x, y) matching the data. + """ + + self._seriesCount = len(self.data) + self._rowLength = max(map(len,self.data)) + + if self.useAbsolute: + # Dimensions are absolute. + normFactor = 1.0 + else: + # Dimensions are normalized to fit. + normWidth = self.groupSpacing + availWidth = self.categoryAxis.scale(0)[1] + normFactor = availWidth / normWidth + + self._positions = [] + for rowNo in range(len(self.data)): + lineRow = [] + for colNo in range(len(self.data[rowNo])): + datum = self.data[rowNo][colNo] + if datum is not None: + (groupX, groupWidth) = self.categoryAxis.scale(colNo) + x = groupX + (0.5 * self.groupSpacing * normFactor) + y = self.valueAxis.scale(0) + height = self.valueAxis.scale(datum) - y + lineRow.append((x, y+height)) + self._positions.append(lineRow) + + + def _innerDrawLabel(self, rowNo, colNo, x, y): + "Draw a label for a given item in the list." + + labelFmt = self.lineLabelFormat + labelValue = self.data[rowNo][colNo] + + if labelFmt is None: + labelText = None + elif type(labelFmt) is StringType: + if labelFmt == 'values': + labelText = self.lineLabelArray[rowNo][colNo] + else: + labelText = labelFmt % labelValue + elif type(labelFmt) is FunctionType: + labelText = labelFmt(labelValue) + elif isinstance(labelFmt, Formatter): + labelText = labelFmt(labelValue) + else: + msg = "Unknown formatter type %s, expected string or function" + raise Exception, msg % labelFmt + + if labelText: + label = self.lineLabels[(rowNo, colNo)] + # Make sure labels are some distance off the data point. + if y > 0: + label.setOrigin(x, y + self.lineLabelNudge) + else: + label.setOrigin(x, y - self.lineLabelNudge) + label.setText(labelText) + else: + label = None + return label + + def drawLabel(self, G, rowNo, colNo, x, y): + '''Draw a label for a given item in the list. + G must have an add method''' + G.add(self._innerDrawLabel(rowNo,colNo,x,y)) + + def makeLines(self): + g = Group() + + labelFmt = self.lineLabelFormat + P = range(len(self._positions)) + if self.reversePlotOrder: P.reverse() + inFill = self.inFill + if inFill: + inFillY = self.categoryAxis._y + inFillX0 = self.valueAxis._x + inFillX1 = inFillX0 + self.categoryAxis._length + inFillG = getattr(self,'_inFillG',g) + + # Iterate over data rows. + for rowNo in P: + row = self._positions[rowNo] + styleCount = len(self.lines) + styleIdx = rowNo % styleCount + rowStyle = self.lines[styleIdx] + rowColor = rowStyle.strokeColor + dash = getattr(rowStyle, 'strokeDashArray', None) + + if hasattr(self.lines[styleIdx], 'strokeWidth'): + strokeWidth = self.lines[styleIdx].strokeWidth + elif hasattr(self.lines, 'strokeWidth'): + strokeWidth = self.lines.strokeWidth + else: + strokeWidth = None + + # Iterate over data columns. + if self.joinedLines: + points = [] + for colNo in range(len(row)): + points += row[colNo] + if inFill: + points = points + [inFillX1,inFillY,inFillX0,inFillY] + inFillG.add(Polygon(points,fillColor=rowColor,strokeColor=rowColor,strokeWidth=0.1)) + else: + line = PolyLine(points,strokeColor=rowColor,strokeLineCap=0,strokeLineJoin=1) + if strokeWidth: + line.strokeWidth = strokeWidth + if dash: + line.strokeDashArray = dash + g.add(line) + + if hasattr(self.lines[styleIdx], 'symbol'): + uSymbol = self.lines[styleIdx].symbol + elif hasattr(self.lines, 'symbol'): + uSymbol = self.lines.symbol + else: + uSymbol = None + + if uSymbol: + for colNo in range(len(row)): + x1, y1 = row[colNo] + symbol = uSymbol2Symbol(uSymbol,x1,y1,rowStyle.strokeColor) + if symbol: g.add(symbol) + + # Draw item labels. + for colNo in range(len(row)): + x1, y1 = row[colNo] + self.drawLabel(g, rowNo, colNo, x1, y1) + + return g + + def draw(self): + "Draws itself." + + vA, cA = self.valueAxis, self.categoryAxis + vA.setPosition(self.x, self.y, self.height) + if vA: vA.joinAxis = cA + if cA: cA.joinAxis = vA + vA.configure(self.data) + + # If zero is in chart, put x axis there, otherwise + # use bottom. + xAxisCrossesAt = vA.scale(0) + if ((xAxisCrossesAt > self.y + self.height) or (xAxisCrossesAt < self.y)): + y = self.y + else: + y = xAxisCrossesAt + + cA.setPosition(self.x, y, self.width) + cA.configure(self.data) + + self.calcPositions() + + g = Group() + g.add(self.makeBackground()) + if self.inFill: + self._inFillG = Group() + g.add(self._inFillG) + + g.add(cA) + g.add(vA) + vA.gridStart = cA._x + vA.gridEnd = cA._x+cA._length + cA.gridStart = vA._y + cA.gridEnd = vA._y+vA._length + cA.makeGrid(g,parent=self) + vA.makeGrid(g,parent=self) + g.add(self.makeLines()) + for a in getattr(self,'annotations',()): g.add(a(self,cA.scale,vA.scale)) + return g + +def _cmpFakeItem(a,b): + '''t, z0, z1, x, y = a[:5]''' + return cmp((-a[1],a[3],a[0],-a[4]),(-b[1],b[3],b[0],-b[4])) + +class _FakeGroup: + def __init__(self): + self._data = [] + + def add(self,what): + if what: self._data.append(what) + + def value(self): + return self._data + + def sort(self): + self._data.sort(_cmpFakeItem) + #for t in self._data: print t + +class HorizontalLineChart3D(HorizontalLineChart): + _attrMap = AttrMap(BASE=HorizontalLineChart, + theta_x = AttrMapValue(isNumber, desc='dx/dz'), + theta_y = AttrMapValue(isNumber, desc='dy/dz'), + zDepth = AttrMapValue(isNumber, desc='depth of an individual series'), + zSpace = AttrMapValue(isNumber, desc='z gap around series'), + ) + theta_x = .5 + theta_y = .5 + zDepth = 10 + zSpace = 3 + + def calcPositions(self): + HorizontalLineChart.calcPositions(self) + nSeries = self._seriesCount + zSpace = self.zSpace + zDepth = self.zDepth + if self.categoryAxis.style=='parallel_3d': + _3d_depth = nSeries*zDepth+(nSeries+1)*zSpace + else: + _3d_depth = zDepth + 2*zSpace + self._3d_dx = self.theta_x*_3d_depth + self._3d_dy = self.theta_y*_3d_depth + + def _calc_z0(self,rowNo): + zSpace = self.zSpace + if self.categoryAxis.style=='parallel_3d': + z0 = rowNo*(self.zDepth+zSpace)+zSpace + else: + z0 = zSpace + return z0 + + def _zadjust(self,x,y,z): + return x+z*self.theta_x, y+z*self.theta_y + + def makeLines(self): + labelFmt = self.lineLabelFormat + P = range(len(self._positions)) + if self.reversePlotOrder: P.reverse() + inFill = self.inFill + assert not inFill, "inFill not supported for 3d yet" + #if inFill: + #inFillY = self.categoryAxis._y + #inFillX0 = self.valueAxis._x + #inFillX1 = inFillX0 + self.categoryAxis._length + #inFillG = getattr(self,'_inFillG',g) + zDepth = self.zDepth + _zadjust = self._zadjust + theta_x = self.theta_x + theta_y = self.theta_y + F = _FakeGroup() + from utils3d import _make_3d_line_info + tileWidth = getattr(self,'_3d_tilewidth',None) + if not tileWidth and self.categoryAxis.style!='parallel_3d': tileWidth = 1 + + # Iterate over data rows. + for rowNo in P: + row = self._positions[rowNo] + n = len(row) + styleCount = len(self.lines) + styleIdx = rowNo % styleCount + rowStyle = self.lines[styleIdx] + rowColor = rowStyle.strokeColor + dash = getattr(rowStyle, 'strokeDashArray', None) + z0 = self._calc_z0(rowNo) + z1 = z0 + zDepth + + if hasattr(self.lines[styleIdx], 'strokeWidth'): + strokeWidth = self.lines[styleIdx].strokeWidth + elif hasattr(self.lines, 'strokeWidth'): + strokeWidth = self.lines.strokeWidth + else: + strokeWidth = None + + # Iterate over data columns. + if self.joinedLines: + if n: + x0, y0 = row[0] + for colNo in xrange(1,n): + x1, y1 = row[colNo] + _make_3d_line_info( F, x0, x1, y0, y1, z0, z1, + theta_x, theta_y, + rowColor, fillColorShaded=None, tileWidth=tileWidth, + strokeColor=None, strokeWidth=None, strokeDashArray=None, + shading=0.1) + x0, y0 = x1, y1 + + if hasattr(self.lines[styleIdx], 'symbol'): + uSymbol = self.lines[styleIdx].symbol + elif hasattr(self.lines, 'symbol'): + uSymbol = self.lines.symbol + else: + uSymbol = None + + if uSymbol: + for colNo in xrange(n): + x1, y1 = row[colNo] + x1, y1 = _zadjust(x1,y1,z0) + symbol = uSymbol2Symbol(uSymbol,x1,y1,rowColor) + if symbol: F.add((2,z0,z0,x1,y1,symbol)) + + # Draw item labels. + for colNo in xrange(n): + x1, y1 = row[colNo] + x1, y1 = _zadjust(x1,y1,z0) + L = self._innerDrawLabel(rowNo, colNo, x1, y1) + if L: F.add((2,z0,z0,x1,y1,L)) + + F.sort() + g = Group() + map(lambda x,a=g.add: a(x[-1]),F.value()) + return g + +class VerticalLineChart(LineChart): + pass + + +def sample1(): + drawing = Drawing(400, 200) + + data = [ + (13, 5, 20, 22, 37, 45, 19, 4), + (5, 20, 46, 38, 23, 21, 6, 14) + ] + + lc = HorizontalLineChart() + + lc.x = 50 + lc.y = 50 + lc.height = 125 + lc.width = 300 + lc.data = data + lc.joinedLines = 1 + lc.lines.symbol = makeMarker('FilledDiamond') + lc.lineLabelFormat = '%2.0f' + + catNames = string.split('Jan Feb Mar Apr May Jun Jul Aug', ' ') + lc.categoryAxis.categoryNames = catNames + lc.categoryAxis.labels.boxAnchor = 'n' + + lc.valueAxis.valueMin = 0 + lc.valueAxis.valueMax = 60 + lc.valueAxis.valueStep = 15 + + drawing.add(lc) + + return drawing + + +class SampleHorizontalLineChart(HorizontalLineChart): + "Sample class overwriting one method to draw additional horizontal lines." + + def demo(self): + """Shows basic use of a line chart.""" + + drawing = Drawing(200, 100) + + data = [ + (13, 5, 20, 22, 37, 45, 19, 4), + (14, 10, 21, 28, 38, 46, 25, 5) + ] + + lc = SampleHorizontalLineChart() + + lc.x = 20 + lc.y = 10 + lc.height = 85 + lc.width = 170 + lc.data = data + lc.strokeColor = colors.white + lc.fillColor = colors.HexColor(0xCCCCCC) + + drawing.add(lc) + + return drawing + + + def makeBackground(self): + g = Group() + + g.add(HorizontalLineChart.makeBackground(self)) + + valAxis = self.valueAxis + valTickPositions = valAxis._tickValues + + for y in valTickPositions: + y = valAxis.scale(y) + g.add(Line(self.x, y, self.x+self.width, y, + strokeColor = self.strokeColor)) + + return g + + + +def sample1a(): + drawing = Drawing(400, 200) + + data = [ + (13, 5, 20, 22, 37, 45, 19, 4), + (5, 20, 46, 38, 23, 21, 6, 14) + ] + + lc = SampleHorizontalLineChart() + + lc.x = 50 + lc.y = 50 + lc.height = 125 + lc.width = 300 + lc.data = data + lc.joinedLines = 1 + lc.strokeColor = colors.white + lc.fillColor = colors.HexColor(0xCCCCCC) + lc.lines.symbol = makeMarker('FilledDiamond') + lc.lineLabelFormat = '%2.0f' + + catNames = string.split('Jan Feb Mar Apr May Jun Jul Aug', ' ') + lc.categoryAxis.categoryNames = catNames + lc.categoryAxis.labels.boxAnchor = 'n' + + lc.valueAxis.valueMin = 0 + lc.valueAxis.valueMax = 60 + lc.valueAxis.valueStep = 15 + + drawing.add(lc) + + return drawing + + +def sample2(): + drawing = Drawing(400, 200) + + data = [ + (13, 5, 20, 22, 37, 45, 19, 4), + (5, 20, 46, 38, 23, 21, 6, 14) + ] + + lc = HorizontalLineChart() + + lc.x = 50 + lc.y = 50 + lc.height = 125 + lc.width = 300 + lc.data = data + lc.joinedLines = 1 + lc.lines.symbol = makeMarker('Smiley') + lc.lineLabelFormat = '%2.0f' + lc.strokeColor = colors.black + lc.fillColor = colors.lightblue + + catNames = string.split('Jan Feb Mar Apr May Jun Jul Aug', ' ') + lc.categoryAxis.categoryNames = catNames + lc.categoryAxis.labels.boxAnchor = 'n' + + lc.valueAxis.valueMin = 0 + lc.valueAxis.valueMax = 60 + lc.valueAxis.valueStep = 15 + + drawing.add(lc) + + return drawing + + +def sample3(): + drawing = Drawing(400, 200) + + data = [ + (13, 5, 20, 22, 37, 45, 19, 4), + (5, 20, 46, 38, 23, 21, 6, 14) + ] + + lc = HorizontalLineChart() + + lc.x = 50 + lc.y = 50 + lc.height = 125 + lc.width = 300 + lc.data = data + lc.joinedLines = 1 + lc.lineLabelFormat = '%2.0f' + lc.strokeColor = colors.black + + lc.lines[0].symbol = makeMarker('Smiley') + lc.lines[1].symbol = NoEntry + lc.lines[0].strokeWidth = 2 + lc.lines[1].strokeWidth = 4 + + catNames = string.split('Jan Feb Mar Apr May Jun Jul Aug', ' ') + lc.categoryAxis.categoryNames = catNames + lc.categoryAxis.labels.boxAnchor = 'n' + + lc.valueAxis.valueMin = 0 + lc.valueAxis.valueMax = 60 + lc.valueAxis.valueStep = 15 + + drawing.add(lc) + + return drawing diff --git a/bin/reportlab/graphics/charts/lineplots.py b/bin/reportlab/graphics/charts/lineplots.py new file mode 100644 index 00000000000..42732dddde3 --- /dev/null +++ b/bin/reportlab/graphics/charts/lineplots.py @@ -0,0 +1,1083 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/lineplots.py +"""This module defines a very preliminary Line Plot example. +""" +__version__=''' $Id: lineplots.py 2659 2005-08-18 10:28:12Z rgbecker $ ''' + +import string, time +from types import FunctionType + +from reportlab.lib import colors +from reportlab.lib.validators import * +from reportlab.lib.attrmap import * +from reportlab.graphics.shapes import Drawing, Group, Rect, Line, PolyLine, Polygon, _SetKeyWordArgs +from reportlab.graphics.widgetbase import Widget, TypedPropertyCollection, PropHolder +from reportlab.graphics.charts.textlabels import Label +from reportlab.graphics.charts.axes import XValueAxis, YValueAxis, AdjYValueAxis, NormalDateXValueAxis +from reportlab.graphics.charts.utils import * +from reportlab.graphics.widgets.markers import uSymbol2Symbol, isSymbol, makeMarker +from reportlab.graphics.widgets.grids import Grid, DoubleGrid, ShadedRect, ShadedPolygon +from reportlab.pdfbase.pdfmetrics import stringWidth, getFont +from reportlab.graphics.charts.areas import PlotArea + +# This might be moved again from here... +class LinePlotProperties(PropHolder): + _attrMap = AttrMap( + strokeWidth = AttrMapValue(isNumber, desc='Width of a line.'), + strokeColor = AttrMapValue(isColorOrNone, desc='Color of a line.'), + strokeDashArray = AttrMapValue(isListOfNumbersOrNone, desc='Dash array of a line.'), + symbol = AttrMapValue(None, desc='Widget placed at data points.'), + shader = AttrMapValue(None, desc='Shader Class.'), + filler = AttrMapValue(None, desc='Filler Class.'), + name = AttrMapValue(isStringOrNone, desc='Name of the line.'), + ) + +class Shader(_SetKeyWordArgs): + _attrMap = AttrMap(BASE=PlotArea, + vertical = AttrMapValue(isBoolean, desc='If true shade to x axis'), + colors = AttrMapValue(SequenceOf(isColorOrNone,lo=2,hi=2), desc='(AxisColor, LineColor)'), + ) + + def shade(self, lp, g, rowNo, rowColor, row): + c = [None,None] + c = getattr(self,'colors',c) or c + if not c[0]: c[0] = getattr(lp,'fillColor',colors.white) + if not c[1]: c[1] = rowColor + +class NoFiller: + def fill(self, lp, g, rowNo, rowColor, points): + pass + +class Filler: + '''mixin providing simple polygon fill''' + _attrMap = AttrMap( + fillColor = AttrMapValue(isColorOrNone, desc='filler interior color'), + strokeColor = AttrMapValue(isColorOrNone, desc='filler edge color'), + strokeWidth = AttrMapValue(isNumberOrNone, desc='filler edge width'), + ) + def __init__(self,**kw): + self.__dict__ = kw + + def fill(self, lp, g, rowNo, rowColor, points): + g.add(Polygon(points, + fillColor=getattr(self,'fillColor',rowColor), + strokeColor=getattr(self,'strokeColor',rowColor), + strokeWidth=getattr(self,'strokeWidth',0.1))) + +class ShadedPolyFiller(Filler,ShadedPolygon): + pass + +class PolyFiller(Filler,Polygon): + pass + +from linecharts import AbstractLineChart +class LinePlot(AbstractLineChart): + """Line plot with multiple lines. + + Both x- and y-axis are value axis (so there are no seperate + X and Y versions of this class). + """ + _attrMap = AttrMap(BASE=PlotArea, + reversePlotOrder = AttrMapValue(isBoolean, desc='If true reverse plot order.'), + lineLabelNudge = AttrMapValue(isNumber, desc='Distance between a data point and its label.'), + lineLabels = AttrMapValue(None, desc='Handle to the list of data point labels.'), + lineLabelFormat = AttrMapValue(None, desc='Formatting string or function used for data point labels.'), + lineLabelArray = AttrMapValue(None, desc='explicit array of line label values, must match size of data if present.'), + joinedLines = AttrMapValue(isNumber, desc='Display data points joined with lines if true.'), + strokeColor = AttrMapValue(isColorOrNone, desc='Color used for background border of plot area.'), + fillColor = AttrMapValue(isColorOrNone, desc='Color used for background interior of plot area.'), + lines = AttrMapValue(None, desc='Handle of the lines.'), + xValueAxis = AttrMapValue(None, desc='Handle of the x axis.'), + yValueAxis = AttrMapValue(None, desc='Handle of the y axis.'), + data = AttrMapValue(None, desc='Data to be plotted, list of (lists of) x/y tuples.'), + annotations = AttrMapValue(None, desc='list of callables, will be called with self, xscale, yscale.'), + ) + + def __init__(self): + PlotArea.__init__(self) + self.reversePlotOrder = 0 + + self.xValueAxis = XValueAxis() + self.yValueAxis = YValueAxis() + + # this defines two series of 3 points. Just an example. + self.data = [ + ((1,1), (2,2), (2.5,1), (3,3), (4,5)), + ((1,2), (2,3), (2.5,2), (3,4), (4,6)) + ] + + self.lines = TypedPropertyCollection(LinePlotProperties) + self.lines.strokeWidth = 1 + self.lines[0].strokeColor = colors.red + self.lines[1].strokeColor = colors.blue + + self.lineLabels = TypedPropertyCollection(Label) + self.lineLabelFormat = None + self.lineLabelArray = None + + # this says whether the origin is inside or outside + # the bar - +10 means put the origin ten points + # above the tip of the bar if value > 0, or ten + # points inside if bar value < 0. This is different + # to label dx/dy which are not dependent on the + # sign of the data. + self.lineLabelNudge = 10 + # if you have multiple series, by default they butt + # together. + + # New line chart attributes. + self.joinedLines = 1 # Connect items with straight lines. + + #private attributes + self._inFill = None + + def demo(self): + """Shows basic use of a line chart.""" + + drawing = Drawing(400, 200) + + data = [ + ((1,1), (2,2), (2.5,1), (3,3), (4,5)), + ((1,2), (2,3), (2.5,2), (3.5,5), (4,6)) + ] + + lp = LinePlot() + + lp.x = 50 + lp.y = 50 + lp.height = 125 + lp.width = 300 + lp.data = data + lp.joinedLines = 1 + lp.lineLabelFormat = '%2.0f' + lp.strokeColor = colors.black + + lp.lines[0].strokeColor = colors.red + lp.lines[0].symbol = makeMarker('FilledCircle') + lp.lines[1].strokeColor = colors.blue + lp.lines[1].symbol = makeMarker('FilledDiamond') + + lp.xValueAxis.valueMin = 0 + lp.xValueAxis.valueMax = 5 + lp.xValueAxis.valueStep = 1 + + lp.yValueAxis.valueMin = 0 + lp.yValueAxis.valueMax = 7 + lp.yValueAxis.valueStep = 1 + + drawing.add(lp) + + return drawing + + + def calcPositions(self): + """Works out where they go. + + Sets an attribute _positions which is a list of + lists of (x, y) matching the data. + """ + + self._seriesCount = len(self.data) + self._rowLength = max(map(len,self.data)) + + self._positions = [] + for rowNo in range(len(self.data)): + line = [] + for colNo in range(len(self.data[rowNo])): + datum = self.data[rowNo][colNo] # x,y value + if type(datum[0]) == type(''): + x = self.xValueAxis.scale(mktime(mkTimeTuple(datum[0]))) + else: + x = self.xValueAxis.scale(datum[0]) + y = self.yValueAxis.scale(datum[1]) + line.append((x, y)) + self._positions.append(line) + + def _innerDrawLabel(self, rowNo, colNo, x, y): + "Draw a label for a given item in the list." + + labelFmt = self.lineLabelFormat + labelValue = self.data[rowNo][colNo][1] ### + + if labelFmt is None: + labelText = None + elif type(labelFmt) is StringType: + if labelFmt == 'values': + labelText = self.lineLabelArray[rowNo][colNo] + else: + labelText = labelFmt % labelValue + elif type(labelFmt) is FunctionType: + labelText = labelFmt(labelValue) + elif isinstance(labelFmt, Formatter): + labelText = labelFmt(labelValue) + else: + msg = "Unknown formatter type %s, expected string or function" + raise Exception, msg % labelFmt + + if labelText: + label = self.lineLabels[(rowNo, colNo)] + #hack to make sure labels are outside the bar + if y > 0: + label.setOrigin(x, y + self.lineLabelNudge) + else: + label.setOrigin(x, y - self.lineLabelNudge) + label.setText(labelText) + else: + label = None + return label + + def drawLabel(self, G, rowNo, colNo, x, y): + '''Draw a label for a given item in the list. + G must have an add method''' + G.add(self._innerDrawLabel(rowNo,colNo,x,y)) + + def makeLines(self): + g = Group() + bubblePlot = getattr(self,'_bubblePlot',None) + if bubblePlot: + yA = self.yValueAxis + xA = self.xValueAxis + bubbleR = min(yA._bubbleRadius,xA._bubbleRadius) + bubbleMax = xA._bubbleMax + + labelFmt = self.lineLabelFormat + + P = range(len(self._positions)) + if self.reversePlotOrder: P.reverse() + inFill = getattr(self,'_inFill',None) + if inFill: + inFillY = self.xValueAxis._y + inFillX0 = self.yValueAxis._x + inFillX1 = inFillX0 + self.xValueAxis._length + inFillG = getattr(self,'_inFillG',g) + # Iterate over data rows. + styleCount = len(self.lines) + for rowNo in P: + row = self._positions[rowNo] + rowStyle = self.lines[rowNo % styleCount] + rowColor = rowStyle.strokeColor + dash = getattr(rowStyle, 'strokeDashArray', None) + + if hasattr(rowStyle, 'strokeWidth'): + width = rowStyle.strokeWidth + elif hasattr(self.lines, 'strokeWidth'): + width = self.lines.strokeWidth + else: + width = None + + # Iterate over data columns. + if self.joinedLines: + points = [] + for xy in row: + points = points + [xy[0], xy[1]] + if inFill: + fpoints = [inFillX0,inFillY] + points + [inFillX1,inFillY] + filler = getattr(rowStyle, 'filler', None) + if filler: + filler.fill(self,inFillG,rowNo,rowColor,fpoints) + else: + inFillG.add(Polygon(fpoints,fillColor=rowColor,strokeColor=rowColor,strokeWidth=width or 0.1)) + if inFill in (None,0,2): + line = PolyLine(points,strokeColor=rowColor,strokeLineCap=0,strokeLineJoin=1) + if width: + line.strokeWidth = width + if dash: + line.strokeDashArray = dash + g.add(line) + + if hasattr(rowStyle, 'symbol'): + uSymbol = rowStyle.symbol + elif hasattr(self.lines, 'symbol'): + uSymbol = self.lines.symbol + else: + uSymbol = None + + if uSymbol: + j = -1 + if bubblePlot: drow = self.data[rowNo] + for xy in row: + j += 1 + symbol = uSymbol2Symbol(uSymbol,xy[0],xy[1],rowColor) + if symbol: + if bubblePlot: + symbol.size = bubbleR*(drow[j][2]/bubbleMax)**0.5 + g.add(symbol) + + # Draw data labels. + for colNo in range(len(row)): + x1, y1 = row[colNo] + self.drawLabel(g, rowNo, colNo, x1, y1) + + shader = getattr(rowStyle, 'shader', None) + if shader: shader.shade(self,g,rowNo,rowColor,row) + + return g + + def draw(self): + yA = self.yValueAxis + xA = self.xValueAxis + if getattr(self,'_bubblePlot',None): + yA._bubblePlot = xA._bubblePlot = 1 + yA.setPosition(self.x, self.y, self.height) + if yA: yA.joinAxis = xA + if xA: xA.joinAxis = yA + yA.configure(self.data) + + # if zero is in chart, put x axis there, otherwise use bottom. + xAxisCrossesAt = yA.scale(0) + if ((xAxisCrossesAt > self.y + self.height) or (xAxisCrossesAt < self.y)): + y = self.y + else: + y = xAxisCrossesAt + + xA.setPosition(self.x, y, self.width) + xA.configure(self.data) + self.calcPositions() + g = Group() + g.add(self.makeBackground()) + if self._inFill: + xA._joinToAxis() + self._inFillG = Group() + g.add(self._inFillG) + g.add(xA) + g.add(yA) + yA.gridStart = xA._x + yA.gridEnd = xA._x+xA._length + xA.gridStart = yA._y + xA.gridEnd = yA._y+yA._length + xA.makeGrid(g,parent=self) + yA.makeGrid(g,parent=self) + g.add(self.makeLines()) + for a in getattr(self,'annotations',()): g.add(a(self,xA.scale,yA.scale)) + return g + +class LinePlot3D(LinePlot): + _attrMap = AttrMap(BASE=LinePlot, + theta_x = AttrMapValue(isNumber, desc='dx/dz'), + theta_y = AttrMapValue(isNumber, desc='dy/dz'), + zDepth = AttrMapValue(isNumber, desc='depth of an individual series'), + zSpace = AttrMapValue(isNumber, desc='z gap around series'), + ) + theta_x = .5 + theta_y = .5 + zDepth = 10 + zSpace = 3 + + def calcPositions(self): + LinePlot.calcPositions(self) + nSeries = self._seriesCount + zSpace = self.zSpace + zDepth = self.zDepth + if self.xValueAxis.style=='parallel_3d': + _3d_depth = nSeries*zDepth+(nSeries+1)*zSpace + else: + _3d_depth = zDepth + 2*zSpace + self._3d_dx = self.theta_x*_3d_depth + self._3d_dy = self.theta_y*_3d_depth + + def _calc_z0(self,rowNo): + zSpace = self.zSpace + if self.xValueAxis.style=='parallel_3d': + z0 = rowNo*(self.zDepth+zSpace)+zSpace + else: + z0 = zSpace + return z0 + + def _zadjust(self,x,y,z): + return x+z*self.theta_x, y+z*self.theta_y + + def makeLines(self): + bubblePlot = getattr(self,'_bubblePlot',None) + assert not bubblePlot, "_bubblePlot not supported for 3d yet" + #if bubblePlot: + # yA = self.yValueAxis + # xA = self.xValueAxis + # bubbleR = min(yA._bubbleRadius,xA._bubbleRadius) + # bubbleMax = xA._bubbleMax + + labelFmt = self.lineLabelFormat + positions = self._positions + + P = range(len(positions)) + if self.reversePlotOrder: P.reverse() + inFill = getattr(self,'_inFill',None) + assert not inFill, "inFill not supported for 3d yet" + #if inFill: + # inFillY = self.xValueAxis._y + # inFillX0 = self.yValueAxis._x + # inFillX1 = inFillX0 + self.xValueAxis._length + # inFillG = getattr(self,'_inFillG',g) + zDepth = self.zDepth + _zadjust = self._zadjust + theta_x = self.theta_x + theta_y = self.theta_y + from linecharts import _FakeGroup + F = _FakeGroup() + + from utils3d import _make_3d_line_info, find_intersections + if self.xValueAxis.style!='parallel_3d': + tileWidth = getattr(self,'_3d_tilewidth',1) + if getattr(self,'_find_intersections',None): + from copy import copy + fpositions = map(copy,positions) + I = find_intersections(fpositions,small=tileWidth) + ic = None + for i,j,x,y in I: + if ic!=i: + ic = i + jc = 0 + else: + jc+=1 + fpositions[i].insert(j+jc,(x,y)) + tileWidth = None + else: + fpositions = positions + else: + tileWidth = None + fpositions = positions + + # Iterate over data rows. + styleCount = len(self.lines) + for rowNo in P: + row = positions[rowNo] + n = len(row) + rowStyle = self.lines[rowNo % styleCount] + rowColor = rowStyle.strokeColor + dash = getattr(rowStyle, 'strokeDashArray', None) + z0 = self._calc_z0(rowNo) + z1 = z0 + zDepth + + if hasattr(rowStyle, 'strokeWidth'): + width = rowStyle.strokeWidth + elif hasattr(self.lines, 'strokeWidth'): + width = self.lines.strokeWidth + else: + width = None + + # Iterate over data columns. + if self.joinedLines: + if n: + frow = fpositions[rowNo] + x0, y0 = frow[0] + for colNo in xrange(1,len(frow)): + x1, y1 = frow[colNo] + _make_3d_line_info( F, x0, x1, y0, y1, z0, z1, + theta_x, theta_y, + rowColor, fillColorShaded=None, tileWidth=tileWidth, + strokeColor=None, strokeWidth=None, strokeDashArray=None, + shading=0.1) + x0, y0 = x1, y1 + + if hasattr(rowStyle, 'symbol'): + uSymbol = rowStyle.symbol + elif hasattr(self.lines, 'symbol'): + uSymbol = self.lines.symbol + else: + uSymbol = None + + if uSymbol: + for xy in row: + x1, y1 = row[colNo] + x1, y1 = _zadjust(x1,y1,z0) + symbol = uSymbol2Symbol(uSymbol,xy[0],xy[1],rowColor) + if symbol: F.add((1,z0,z0,x1,y1,symbol)) + + # Draw data labels. + for colNo in xrange(n): + x1, y1 = row[colNo] + x1, y1 = _zadjust(x1,y1,z0) + L = self._innerDrawLabel(rowNo, colNo, x1, y1) + if L: F.add((2,z0,z0,x1,y1,L)) + + F.sort() + g = Group() + map(lambda x,a=g.add: a(x[-1]),F.value()) + return g + +_monthlyIndexData = [[(19971202, 100.0), + (19971231, 100.1704367), + (19980131, 101.5639577), + (19980228, 102.1879927), + (19980331, 101.6337257), + (19980430, 102.7640446), + (19980531, 102.9198038), + (19980630, 103.25938789999999), + (19980731, 103.2516421), + (19980831, 105.4744329), + (19980930, 109.3242705), + (19981031, 111.9859291), + (19981130, 110.9184642), + (19981231, 110.9184642), + (19990131, 111.9882532), + (19990228, 109.7912614), + (19990331, 110.24189629999999), + (19990430, 110.4279321), + (19990531, 109.33955469999999), + (19990630, 108.2341748), + (19990731, 110.21294469999999), + (19990831, 110.9683062), + (19990930, 112.4425371), + (19991031, 112.7314032), + (19991130, 112.3509645), + (19991231, 112.3660659), + (20000131, 110.9255248), + (20000229, 110.5266306), + (20000331, 113.3116101), + (20000430, 111.0449133), + (20000531, 111.702717), + (20000630, 113.5832178)], + [(19971202, 100.0), + (19971231, 100.0), + (19980131, 100.8), + (19980228, 102.0), + (19980331, 101.9), + (19980430, 103.0), + (19980531, 103.0), + (19980630, 103.1), + (19980731, 103.1), + (19980831, 102.8), + (19980930, 105.6), + (19981031, 108.3), + (19981130, 108.1), + (19981231, 111.9), + (19990131, 113.1), + (19990228, 110.2), + (19990331, 111.8), + (19990430, 112.3), + (19990531, 110.1), + (19990630, 109.3), + (19990731, 111.2), + (19990831, 111.7), + (19990930, 112.6), + (19991031, 113.2), + (19991130, 113.9), + (19991231, 115.4), + (20000131, 112.7), + (20000229, 113.9), + (20000331, 115.8), + (20000430, 112.2), + (20000531, 112.6), + (20000630, 114.6)]] + +class GridLinePlot(LinePlot): + """A customized version of LinePlot. + It uses NormalDateXValueAxis() and AdjYValueAxis() for the X and Y axes. + The chart has a default grid background with thin horizontal lines + aligned with the tickmarks (and labels). You can change the back- + ground to be any Grid or ShadedRect, or scale the whole chart. + If you do provide a background, you can specify the colours of the + stripes with 'background.stripeColors'. + """ + + _attrMap = AttrMap(BASE=LinePlot, + background = AttrMapValue(None, desc='Background for chart area (now Grid or ShadedRect).'), + scaleFactor = AttrMapValue(isNumberOrNone, desc='Scalefactor to apply to whole drawing.'), + ) + + def __init__(self): + from reportlab.lib import colors + LinePlot.__init__(self) + self.xValueAxis = NormalDateXValueAxis() + self.yValueAxis = AdjYValueAxis() + self.scaleFactor = None + self.background = Grid() + self.background.orientation = 'horizontal' + self.background.useRects = 0 + self.background.useLines = 1 + self.background.strokeWidth = 0.5 + self.background.strokeColor = colors.black + self.data = _monthlyIndexData + + def demo(self,drawing=None): + from reportlab.lib import colors + if not drawing: + drawing = Drawing(400, 200) + lp = AdjLinePlot() + lp.x = 50 + lp.y = 50 + lp.height = 125 + lp.width = 300 + lp.data = _monthlyIndexData + lp.joinedLines = 1 + lp.strokeColor = colors.black + c0 = colors.PCMYKColor(100,65,0,30, spotName='PANTONE 288 CV', density=100) + lp.lines[0].strokeColor = c0 + lp.lines[0].strokeWidth = 2 + lp.lines[0].strokeDashArray = None + c1 = colors.PCMYKColor(0,79,91,0, spotName='PANTONE Wm Red CV', density=100) + lp.lines[1].strokeColor = c1 + lp.lines[1].strokeWidth = 1 + lp.lines[1].strokeDashArray = [3,1] + lp.xValueAxis.labels.fontSize = 10 + lp.xValueAxis.labels.textAnchor = 'start' + lp.xValueAxis.labels.boxAnchor = 'w' + lp.xValueAxis.labels.angle = -45 + lp.xValueAxis.labels.dx = 0 + lp.xValueAxis.labels.dy = -8 + lp.xValueAxis.xLabelFormat = '{mm}/{yy}' + lp.yValueAxis.labelTextFormat = '%5d%% ' + lp.yValueAxis.tickLeft = 5 + lp.yValueAxis.labels.fontSize = 10 + lp.background = Grid() + lp.background.stripeColors = [colors.pink, colors.lightblue] + lp.background.orientation = 'vertical' + drawing.add(lp,'plot') + return drawing + + def draw(self): + xva, yva = self.xValueAxis, self.yValueAxis + if xva: xva.joinAxis = yva + if yva: yva.joinAxis = xva + + yva.setPosition(self.x, self.y, self.height) + yva.configure(self.data) + + # if zero is in chart, put x axis there, otherwise + # use bottom. + xAxisCrossesAt = yva.scale(0) + if ((xAxisCrossesAt > self.y + self.height) or (xAxisCrossesAt < self.y)): + y = self.y + else: + y = xAxisCrossesAt + + xva.setPosition(self.x, y, self.width) + xva.configure(self.data) + + back = self.background + if isinstance(back, Grid): + if back.orientation == 'vertical' and xva._tickValues: + xpos = map(xva.scale, [xva._valueMin] + xva._tickValues) + steps = [] + for i in range(len(xpos)-1): + steps.append(xpos[i+1] - xpos[i]) + back.deltaSteps = steps + elif back.orientation == 'horizontal' and yva._tickValues: + ypos = map(yva.scale, [yva._valueMin] + yva._tickValues) + steps = [] + for i in range(len(ypos)-1): + steps.append(ypos[i+1] - ypos[i]) + back.deltaSteps = steps + elif isinstance(back, DoubleGrid): + # Ideally, these lines would not be needed... + back.grid0.x = self.x + back.grid0.y = self.y + back.grid0.width = self.width + back.grid0.height = self.height + back.grid1.x = self.x + back.grid1.y = self.y + back.grid1.width = self.width + back.grid1.height = self.height + + # some room left for optimization... + if back.grid0.orientation == 'vertical' and xva._tickValues: + xpos = map(xva.scale, [xva._valueMin] + xva._tickValues) + steps = [] + for i in range(len(xpos)-1): + steps.append(xpos[i+1] - xpos[i]) + back.grid0.deltaSteps = steps + elif back.grid0.orientation == 'horizontal' and yva._tickValues: + ypos = map(yva.scale, [yva._valueMin] + yva._tickValues) + steps = [] + for i in range(len(ypos)-1): + steps.append(ypos[i+1] - ypos[i]) + back.grid0.deltaSteps = steps + if back.grid1.orientation == 'vertical' and xva._tickValues: + xpos = map(xva.scale, [xva._valueMin] + xva._tickValues) + steps = [] + for i in range(len(xpos)-1): + steps.append(xpos[i+1] - xpos[i]) + back.grid1.deltaSteps = steps + elif back.grid1.orientation == 'horizontal' and yva._tickValues: + ypos = map(yva.scale, [yva._valueMin] + yva._tickValues) + steps = [] + for i in range(len(ypos)-1): + steps.append(ypos[i+1] - ypos[i]) + back.grid1.deltaSteps = steps + + self.calcPositions() + + width, height, scaleFactor = self.width, self.height, self.scaleFactor + if scaleFactor and scaleFactor!=1: + #g = Drawing(scaleFactor*width, scaleFactor*height) + g.transform = (scaleFactor, 0, 0, scaleFactor,0,0) + else: + g = Group() + + g.add(self.makeBackground()) + g.add(self.xValueAxis) + g.add(self.yValueAxis) + g.add(self.makeLines()) + + return g + +class AreaLinePlot(LinePlot): + '''we're given data in the form [(X1,Y11,..Y1M)....(Xn,Yn1,...YnM)]'''#' + def __init__(self): + LinePlot.__init__(self) + self._inFill = 1 + self.reversePlotOrder = 1 + self.data = [(1,20,100,30),(2,11,50,15),(3,15,70,40)] + + def draw(self): + try: + odata = self.data + n = len(odata) + m = len(odata[0]) + S = n*[0] + self.data = [] + for i in xrange(1,m): + D = [] + for j in xrange(n): + S[j] = S[j] + odata[j][i] + D.append((odata[j][0],S[j])) + self.data.append(D) + return LinePlot.draw(self) + finally: + self.data = odata + +class SplitLinePlot(AreaLinePlot): + def __init__(self): + AreaLinePlot.__init__(self) + self.xValueAxis = NormalDateXValueAxis() + self.yValueAxis = AdjYValueAxis() + self.data=[(20030601,0.95,0.05,0.0),(20030701,0.95,0.05,0.0),(20030801,0.95,0.05,0.0),(20030901,0.95,0.05,0.0),(20031001,0.95,0.05,0.0),(20031101,0.95,0.05,0.0),(20031201,0.95,0.05,0.0),(20040101,0.95,0.05,0.0),(20040201,0.95,0.05,0.0),(20040301,0.95,0.05,0.0),(20040401,0.95,0.05,0.0),(20040501,0.95,0.05,0.0),(20040601,0.95,0.05,0.0),(20040701,0.95,0.05,0.0),(20040801,0.95,0.05,0.0),(20040901,0.95,0.05,0.0),(20041001,0.95,0.05,0.0),(20041101,0.95,0.05,0.0),(20041201,0.95,0.05,0.0),(20050101,0.95,0.05,0.0),(20050201,0.95,0.05,0.0),(20050301,0.95,0.05,0.0),(20050401,0.95,0.05,0.0),(20050501,0.95,0.05,0.0),(20050601,0.95,0.05,0.0),(20050701,0.95,0.05,0.0),(20050801,0.95,0.05,0.0),(20050901,0.95,0.05,0.0),(20051001,0.95,0.05,0.0),(20051101,0.95,0.05,0.0),(20051201,0.95,0.05,0.0),(20060101,0.95,0.05,0.0),(20060201,0.95,0.05,0.0),(20060301,0.95,0.05,0.0),(20060401,0.95,0.05,0.0),(20060501,0.95,0.05,0.0),(20060601,0.95,0.05,0.0),(20060701,0.95,0.05,0.0),(20060801,0.95,0.05,0.0),(20060901,0.95,0.05,0.0),(20061001,0.95,0.05,0.0),(20061101,0.95,0.05,0.0),(20061201,0.95,0.05,0.0),(20070101,0.95,0.05,0.0),(20070201,0.95,0.05,0.0),(20070301,0.95,0.05,0.0),(20070401,0.95,0.05,0.0),(20070501,0.95,0.05,0.0),(20070601,0.95,0.05,0.0),(20070701,0.95,0.05,0.0),(20070801,0.95,0.05,0.0),(20070901,0.95,0.05,0.0),(20071001,0.95,0.05,0.0),(20071101,0.95,0.05,0.0),(20071201,0.95,0.05,0.0),(20080101,0.95,0.05,0.0),(20080201,0.95,0.05,0.0),(20080301,0.95,0.05,0.0),(20080401,0.95,0.05,0.0),(20080501,0.95,0.05,0.0),(20080601,0.95,0.05,0.0),(20080701,0.95,0.05,0.0),(20080801,0.95,0.05,0.0),(20080901,0.95,0.05,0.0),(20081001,0.95,0.05,0.0),(20081101,0.95,0.05,0.0),(20081201,0.95,0.05,0.0),(20090101,0.95,0.05,0.0),(20090201,0.91,0.09,0.0),(20090301,0.91,0.09,0.0),(20090401,0.91,0.09,0.0),(20090501,0.91,0.09,0.0),(20090601,0.91,0.09,0.0),(20090701,0.91,0.09,0.0),(20090801,0.91,0.09,0.0),(20090901,0.91,0.09,0.0),(20091001,0.91,0.09,0.0),(20091101,0.91,0.09,0.0),(20091201,0.91,0.09,0.0),(20100101,0.91,0.09,0.0),(20100201,0.81,0.19,0.0),(20100301,0.81,0.19,0.0),(20100401,0.81,0.19,0.0),(20100501,0.81,0.19,0.0),(20100601,0.81,0.19,0.0),(20100701,0.81,0.19,0.0),(20100801,0.81,0.19,0.0),(20100901,0.81,0.19,0.0),(20101001,0.81,0.19,0.0),(20101101,0.81,0.19,0.0),(20101201,0.81,0.19,0.0),(20110101,0.81,0.19,0.0),(20110201,0.72,0.28,0.0),(20110301,0.72,0.28,0.0),(20110401,0.72,0.28,0.0),(20110501,0.72,0.28,0.0),(20110601,0.72,0.28,0.0),(20110701,0.72,0.28,0.0),(20110801,0.72,0.28,0.0),(20110901,0.72,0.28,0.0),(20111001,0.72,0.28,0.0),(20111101,0.72,0.28,0.0),(20111201,0.72,0.28,0.0),(20120101,0.72,0.28,0.0),(20120201,0.53,0.47,0.0),(20120301,0.53,0.47,0.0),(20120401,0.53,0.47,0.0),(20120501,0.53,0.47,0.0),(20120601,0.53,0.47,0.0),(20120701,0.53,0.47,0.0),(20120801,0.53,0.47,0.0),(20120901,0.53,0.47,0.0),(20121001,0.53,0.47,0.0),(20121101,0.53,0.47,0.0),(20121201,0.53,0.47,0.0),(20130101,0.53,0.47,0.0),(20130201,0.44,0.56,0.0),(20130301,0.44,0.56,0.0),(20130401,0.44,0.56,0.0),(20130501,0.44,0.56,0.0),(20130601,0.44,0.56,0.0),(20130701,0.44,0.56,0.0),(20130801,0.44,0.56,0.0),(20130901,0.44,0.56,0.0),(20131001,0.44,0.56,0.0),(20131101,0.44,0.56,0.0),(20131201,0.44,0.56,0.0),(20140101,0.44,0.56,0.0),(20140201,0.36,0.5,0.14),(20140301,0.36,0.5,0.14),(20140401,0.36,0.5,0.14),(20140501,0.36,0.5,0.14),(20140601,0.36,0.5,0.14),(20140701,0.36,0.5,0.14),(20140801,0.36,0.5,0.14),(20140901,0.36,0.5,0.14),(20141001,0.36,0.5,0.14),(20141101,0.36,0.5,0.14),(20141201,0.36,0.5,0.14),(20150101,0.36,0.5,0.14),(20150201,0.3,0.41,0.29),(20150301,0.3,0.41,0.29),(20150401,0.3,0.41,0.29),(20150501,0.3,0.41,0.29),(20150601,0.3,0.41,0.29),(20150701,0.3,0.41,0.29),(20150801,0.3,0.41,0.29),(20150901,0.3,0.41,0.29),(20151001,0.3,0.41,0.29),(20151101,0.3,0.41,0.29),(20151201,0.3,0.41,0.29),(20160101,0.3,0.41,0.29),(20160201,0.26,0.36,0.38),(20160301,0.26,0.36,0.38),(20160401,0.26,0.36,0.38),(20160501,0.26,0.36,0.38),(20160601,0.26,0.36,0.38),(20160701,0.26,0.36,0.38),(20160801,0.26,0.36,0.38),(20160901,0.26,0.36,0.38),(20161001,0.26,0.36,0.38),(20161101,0.26,0.36,0.38),(20161201,0.26,0.36,0.38),(20170101,0.26,0.36,0.38),(20170201,0.2,0.3,0.5),(20170301,0.2,0.3,0.5),(20170401,0.2,0.3,0.5),(20170501,0.2,0.3,0.5),(20170601,0.2,0.3,0.5),(20170701,0.2,0.3,0.5),(20170801,0.2,0.3,0.5),(20170901,0.2,0.3,0.5),(20171001,0.2,0.3,0.5),(20171101,0.2,0.3,0.5),(20171201,0.2,0.3,0.5),(20180101,0.2,0.3,0.5),(20180201,0.13,0.37,0.5),(20180301,0.13,0.37,0.5),(20180401,0.13,0.37,0.5),(20180501,0.13,0.37,0.5),(20180601,0.13,0.37,0.5),(20180701,0.13,0.37,0.5),(20180801,0.13,0.37,0.5),(20180901,0.13,0.37,0.5),(20181001,0.13,0.37,0.5),(20181101,0.13,0.37,0.5),(20181201,0.13,0.37,0.5),(20190101,0.13,0.37,0.5),(20190201,0.1,0.4,0.5),(20190301,0.1,0.4,0.5),(20190401,0.1,0.4,0.5),(20190501,0.1,0.4,0.5),(20190601,0.1,0.4,0.5),(20190701,0.1,0.4,0.5),(20190801,0.1,0.4,0.5),(20190901,0.1,0.4,0.5),(20191001,0.1,0.4,0.5),(20191101,0.1,0.4,0.5),(20191201,0.1,0.4,0.5),(20200101,0.1,0.4,0.5)] + self.yValueAxis.requiredRange = None + self.yValueAxis.leftAxisPercent = 0 + self.yValueAxis.leftAxisOrigShiftMin = 0 + self.yValueAxis.leftAxisOrigShiftIPC = 0 + self.lines[0].strokeColor = colors.toColor(0x0033cc) + self.lines[1].strokeColor = colors.toColor(0x99c3ff) + self.lines[2].strokeColor = colors.toColor(0xCC0033) + +def _maxWidth(T, fontName, fontSize): + '''return max stringWidth for the list of strings T''' + if type(T) not in (type(()),type([])): T = (T,) + T = filter(None,T) + return T and max(map(lambda t,sW=stringWidth,fN=fontName, fS=fontSize: sW(t,fN,fS),T)) or 0 + +class ScatterPlot(LinePlot): + """A scatter plot widget""" + + _attrMap = AttrMap(BASE=LinePlot, + width = AttrMapValue(isNumber, desc="Width of the area inside the axes"), + height = AttrMapValue(isNumber, desc="Height of the area inside the axes"), + outerBorderOn = AttrMapValue(isBoolean, desc="Is there an outer border (continuation of axes)"), + outerBorderColor = AttrMapValue(isColorOrNone, desc="Color of outer border (if any)"), + background = AttrMapValue(isColorOrNone, desc="Background color (if any)"), + labelOffset = AttrMapValue(isNumber, desc="Space between label and Axis (or other labels)"), + axisTickLengths = AttrMapValue(isNumber, desc="Lenth of the ticks on both axes"), + axisStrokeWidth = AttrMapValue(isNumber, desc="Stroke width for both axes"), + xLabel = AttrMapValue(isString, desc="Label for the whole X-Axis"), + yLabel = AttrMapValue(isString, desc="Label for the whole Y-Axis"), + data = AttrMapValue(isAnything, desc='Data points - a list of x/y tuples.'), + strokeColor = AttrMapValue(isColorOrNone, desc='Color used for border of plot area.'), + fillColor = AttrMapValue(isColorOrNone, desc='Color used for background interior of plot area.'), + leftPadding = AttrMapValue(isNumber, desc='Padding on left of drawing'), + rightPadding = AttrMapValue(isNumber, desc='Padding on right of drawing'), + topPadding = AttrMapValue(isNumber, desc='Padding at top of drawing'), + bottomPadding = AttrMapValue(isNumber, desc='Padding at bottom of drawing'), + ) + + def __init__(self): + LinePlot.__init__(self) + self.width = 142 + self.height = 77 + self.outerBorderOn = 1 + self.outerBorderColor = colors.black + self.background = None + + _labelOffset = 3 + _axisTickLengths = 2 + _axisStrokeWidth = 0.5 + + self.yValueAxis.valueMin = None + self.yValueAxis.valueMax = None + self.yValueAxis.valueStep = None + self.yValueAxis.labelTextFormat = '%s' + + self.xLabel="X Lable" + self.xValueAxis.labels.fontSize = 6 + + self.yLabel="Y Lable" + self.yValueAxis.labels.fontSize = 6 + + self.data =[((0.030, 62.73), + (0.074, 54.363), + (1.216, 17.964)), + + ((1.360, 11.621), + (1.387, 50.011), + (1.428, 68.953)), + + ((1.444, 86.888), + (1.754, 35.58), + (1.766, 36.05))] + + #values for lineplot + self.joinedLines = 0 + self.fillColor = self.background + + self.leftPadding=5 + self.rightPadding=10 + self.topPadding=5 + self.bottomPadding=5 + + self.x = self.leftPadding+_axisTickLengths+(_labelOffset*2) + self.x=self.x+_maxWidth(str(self.yValueAxis.valueMax), self.yValueAxis.labels.fontName, self.yValueAxis.labels.fontSize) + self.y = self.bottomPadding+_axisTickLengths+_labelOffset+self.xValueAxis.labels.fontSize + + self.xValueAxis.labels.dy = -_labelOffset + self.xValueAxis.tickDown = _axisTickLengths + self.xValueAxis.strokeWidth = _axisStrokeWidth + self.xValueAxis.rangeRound='both' + self.yValueAxis.labels.dx = -_labelOffset + self.yValueAxis.tickLeft = _axisTickLengths + self.yValueAxis.strokeWidth = _axisStrokeWidth + self.yValueAxis.rangeRound='both' + + self.lineLabelFormat="%.2f" + self.lineLabels.fontSize = 5 + self.lineLabels.boxAnchor = 'e' + self.lineLabels.dx = -2 + self.lineLabelNudge = 0 + self.lines.symbol=makeMarker('FilledCircle',size=3) + self.lines[1].symbol=makeMarker('FilledDiamond',size=3) + self.lines[2].symbol=makeMarker('FilledSquare',size=3) + self.lines[2].strokeColor = colors.green + + def _getDrawingDimensions(self): + tx = self.leftPadding+self.yValueAxis.tickLeft+(self.yValueAxis.labels.dx*2)+self.xValueAxis.labels.fontSize + tx=tx+(5*_maxWidth(str(self.yValueAxis.valueMax), self.yValueAxis.labels.fontName, self.yValueAxis.labels.fontSize)) + tx=tx+self.width+self.rightPadding + t=('%.2f%%'%self.xValueAxis.valueMax) + tx=tx+(_maxWidth(t, self.yValueAxis.labels.fontName, self.yValueAxis.labels.fontSize)) + ty = self.bottomPadding+self.xValueAxis.tickDown+(self.xValueAxis.labels.dy*2)+(self.xValueAxis.labels.fontSize*2) + ty=ty+self.yValueAxis.labels.fontSize+self.height+self.topPadding + #print (tx, ty) + return (tx,ty) + + def demo(self,drawing=None): + if not drawing: + tx,ty=self._getDrawingDimensions() + drawing = Drawing(tx,ty) + drawing.add(self.draw()) + return drawing + + def draw(self): + ascent=getFont(self.xValueAxis.labels.fontName).face.ascent + if ascent==0: + ascent=0.718 # default (from helvetica) + ascent=ascent*self.xValueAxis.labels.fontSize # normalize + + #basic LinePlot - does the Axes, Ticks etc + lp = LinePlot.draw(self) + + xLabel = self.xLabel + if xLabel: #Overall label for the X-axis + xl=Label() + xl.x = (self.x+self.width)/2.0 + xl.y = 0 + xl.fontName = self.xValueAxis.labels.fontName + xl.fontSize = self.xValueAxis.labels.fontSize + xl.setText(xLabel) + lp.add(xl) + + yLabel = self.yLabel + if yLabel: #Overall label for the Y-axis + yl=Label() + yl.angle = 90 + yl.x = 0 + yl.y = (self.y+self.height/2.0) + yl.fontName = self.yValueAxis.labels.fontName + yl.fontSize = self.yValueAxis.labels.fontSize + yl.setText(yLabel) + lp.add(yl) + + # do a bounding box - in the same style as the axes + if self.outerBorderOn: + lp.add(Rect(self.x, self.y, self.width, self.height, + strokeColor = self.outerBorderColor, + strokeWidth = self.yValueAxis.strokeWidth, + fillColor = None)) + + lp.shift(self.leftPadding, self.bottomPadding) + + return lp + +def sample1a(): + "A line plot with non-equidistant points in x-axis." + + drawing = Drawing(400, 200) + + data = [ + ((1,1), (2,2), (2.5,1), (3,3), (4,5)), + ((1,2), (2,3), (2.5,2), (3.5,5), (4,6)) + ] + + lp = LinePlot() + + lp.x = 50 + lp.y = 50 + lp.height = 125 + lp.width = 300 + lp.data = data + lp.joinedLines = 1 + lp.strokeColor = colors.black + + lp.lines.symbol = makeMarker('UK_Flag') + + lp.lines[0].strokeWidth = 2 + lp.lines[1].strokeWidth = 4 + + lp.xValueAxis.valueMin = 0 + lp.xValueAxis.valueMax = 5 + lp.xValueAxis.valueStep = 1 + + lp.yValueAxis.valueMin = 0 + lp.yValueAxis.valueMax = 7 + lp.yValueAxis.valueStep = 1 + + drawing.add(lp) + + return drawing + + +def sample1b(): + "A line plot with non-equidistant points in x-axis." + + drawing = Drawing(400, 200) + + data = [ + ((1,1), (2,2), (2.5,1), (3,3), (4,5)), + ((1,2), (2,3), (2.5,2), (3.5,5), (4,6)) + ] + + lp = LinePlot() + + lp.x = 50 + lp.y = 50 + lp.height = 125 + lp.width = 300 + lp.data = data + lp.joinedLines = 1 + lp.lines.symbol = makeMarker('Circle') + lp.lineLabelFormat = '%2.0f' + lp.strokeColor = colors.black + + lp.xValueAxis.valueMin = 0 + lp.xValueAxis.valueMax = 5 + lp.xValueAxis.valueSteps = [1, 2, 2.5, 3, 4, 5] + lp.xValueAxis.labelTextFormat = '%2.1f' + + lp.yValueAxis.valueMin = 0 + lp.yValueAxis.valueMax = 7 + lp.yValueAxis.valueStep = 1 + + drawing.add(lp) + + return drawing + + +def sample1c(): + "A line plot with non-equidistant points in x-axis." + + drawing = Drawing(400, 200) + + data = [ + ((1,1), (2,2), (2.5,1), (3,3), (4,5)), + ((1,2), (2,3), (2.5,2), (3.5,5), (4,6)) + ] + + lp = LinePlot() + + lp.x = 50 + lp.y = 50 + lp.height = 125 + lp.width = 300 + lp.data = data + lp.joinedLines = 1 + lp.lines[0].symbol = makeMarker('FilledCircle') + lp.lines[1].symbol = makeMarker('Circle') + lp.lineLabelFormat = '%2.0f' + lp.strokeColor = colors.black + + lp.xValueAxis.valueMin = 0 + lp.xValueAxis.valueMax = 5 + lp.xValueAxis.valueSteps = [1, 2, 2.5, 3, 4, 5] + lp.xValueAxis.labelTextFormat = '%2.1f' + + lp.yValueAxis.valueMin = 0 + lp.yValueAxis.valueMax = 7 + lp.yValueAxis.valueSteps = [1, 2, 3, 5, 6] + + drawing.add(lp) + + return drawing + + +def preprocessData(series): + "Convert date strings into seconds and multiply values by 100." + + return map(lambda x: (str2seconds(x[0]), x[1]*100), series) + + +def sample2(): + "A line plot with non-equidistant points in x-axis." + + drawing = Drawing(400, 200) + + data = [ + (('25/11/1991',1), + ('30/11/1991',1.000933333), + ('31/12/1991',1.0062), + ('31/01/1992',1.0112), + ('29/02/1992',1.0158), + ('31/03/1992',1.020733333), + ('30/04/1992',1.026133333), + ('31/05/1992',1.030266667), + ('30/06/1992',1.034466667), + ('31/07/1992',1.038733333), + ('31/08/1992',1.0422), + ('30/09/1992',1.045533333), + ('31/10/1992',1.049866667), + ('30/11/1992',1.054733333), + ('31/12/1992',1.061), + ), + ] + + data[0] = preprocessData(data[0]) + + lp = LinePlot() + + lp.x = 50 + lp.y = 50 + lp.height = 125 + lp.width = 300 + lp.data = data + lp.joinedLines = 1 + lp.lines.symbol = makeMarker('FilledDiamond') + lp.strokeColor = colors.black + + start = mktime(mkTimeTuple('25/11/1991')) + t0 = mktime(mkTimeTuple('30/11/1991')) + t1 = mktime(mkTimeTuple('31/12/1991')) + t2 = mktime(mkTimeTuple('31/03/1992')) + t3 = mktime(mkTimeTuple('30/06/1992')) + t4 = mktime(mkTimeTuple('30/09/1992')) + end = mktime(mkTimeTuple('31/12/1992')) + lp.xValueAxis.valueMin = start + lp.xValueAxis.valueMax = end + lp.xValueAxis.valueSteps = [start, t0, t1, t2, t3, t4, end] + lp.xValueAxis.labelTextFormat = seconds2str + lp.xValueAxis.labels[1].dy = -20 + lp.xValueAxis.labels[2].dy = -35 + + lp.yValueAxis.labelTextFormat = '%4.2f' + lp.yValueAxis.valueMin = 100 + lp.yValueAxis.valueMax = 110 + lp.yValueAxis.valueStep = 2 + + drawing.add(lp) + + return drawing diff --git a/bin/reportlab/graphics/charts/markers.py b/bin/reportlab/graphics/charts/markers.py new file mode 100644 index 00000000000..cb3493cb36b --- /dev/null +++ b/bin/reportlab/graphics/charts/markers.py @@ -0,0 +1,81 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/markers.py +""" +This modules defines a collection of markers used in charts. + +The make* functions return a simple shape or a widget as for +the smiley. +""" +__version__=''' $Id: markers.py 2385 2004-06-17 15:26:05Z rgbecker $ ''' +from reportlab.lib import colors +from reportlab.graphics.shapes import Rect, Line, Circle, Polygon +from reportlab.graphics.widgets.signsandsymbols import SmileyFace + + +def makeEmptySquare(x, y, size, color): + "Make an empty square marker." + + d = size/2.0 + rect = Rect(x-d, y-d, 2*d, 2*d) + rect.strokeColor = color + rect.fillColor = None + + return rect + + +def makeFilledSquare(x, y, size, color): + "Make a filled square marker." + + d = size/2.0 + rect = Rect(x-d, y-d, 2*d, 2*d) + rect.strokeColor = color + rect.fillColor = color + + return rect + + +def makeFilledDiamond(x, y, size, color): + "Make a filled diamond marker." + + d = size/2.0 + poly = Polygon((x-d,y, x,y+d, x+d,y, x,y-d)) + poly.strokeColor = color + poly.fillColor = color + + return poly + + +def makeEmptyCircle(x, y, size, color): + "Make a hollow circle marker." + + d = size/2.0 + circle = Circle(x, y, d) + circle.strokeColor = color + circle.fillColor = colors.white + + return circle + + +def makeFilledCircle(x, y, size, color): + "Make a hollow circle marker." + + d = size/2.0 + circle = Circle(x, y, d) + circle.strokeColor = color + circle.fillColor = color + + return circle + + +def makeSmiley(x, y, size, color): + "Make a smiley marker." + + d = size + s = SmileyFace() + s.fillColor = color + s.x = x-d + s.y = y-d + s.size = d*2 + + return s
\ No newline at end of file diff --git a/bin/reportlab/graphics/charts/piecharts.py b/bin/reportlab/graphics/charts/piecharts.py new file mode 100644 index 00000000000..f8646086b8e --- /dev/null +++ b/bin/reportlab/graphics/charts/piecharts.py @@ -0,0 +1,1279 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/piecharts.py +# experimental pie chart script. Two types of pie - one is a monolithic +#widget with all top-level properties, the other delegates most stuff to +#a wedges collection whic lets you customize the group or every individual +#wedge. + +"""Basic Pie Chart class. + +This permits you to customize and pop out individual wedges; +supports elliptical and circular pies. +""" +__version__=''' $Id: piecharts.py 2743 2005-12-12 15:51:29Z rgbecker $ ''' + +import copy +from math import sin, cos, pi + +from reportlab.lib import colors +from reportlab.lib.validators import isColor, isNumber, isListOfNumbersOrNone,\ + isListOfNumbers, isColorOrNone, isString,\ + isListOfStringsOrNone, OneOf, SequenceOf,\ + isBoolean, isListOfColors, isNumberOrNone,\ + isNoneOrListOfNoneOrStrings, isTextAnchor,\ + isNoneOrListOfNoneOrNumbers, isBoxAnchor,\ + isStringOrNone, NoneOr +from reportlab.graphics.widgets.markers import uSymbol2Symbol, isSymbol +from reportlab.lib.attrmap import * +from reportlab.pdfgen.canvas import Canvas +from reportlab.graphics.shapes import Group, Drawing, Ellipse, Wedge, String, STATE_DEFAULTS, ArcPath, Polygon, Rect, PolyLine +from reportlab.graphics.widgetbase import Widget, TypedPropertyCollection, PropHolder +from reportlab.graphics.charts.areas import PlotArea +from textlabels import Label + +_ANGLE2BOXANCHOR={0:'w', 45:'sw', 90:'s', 135:'se', 180:'e', 225:'ne', 270:'n', 315: 'nw', -45: 'nw'} +_ANGLE2RBOXANCHOR={0:'e', 45:'ne', 90:'n', 135:'nw', 180:'w', 225:'sw', 270:'s', 315: 'se', -45: 'se'} +class WedgeLabel(Label): + def _checkDXY(self,ba): + pass + def _getBoxAnchor(self): + na = (int((self._pmv%360)/45.)*45)%360 + if not (na % 90): # we have a right angle case + da = (self._pmv - na) % 360 + if abs(da)>5: + na = na + (da>0 and 45 or -45) + ba = (getattr(self,'_anti',None) and _ANGLE2RBOXANCHOR or _ANGLE2BOXANCHOR)[na] + self._checkDXY(ba) + return ba + +class WedgeProperties(PropHolder): + """This holds descriptive information about the wedges in a pie chart. + + It is not to be confused with the 'wedge itself'; this just holds + a recipe for how to format one, and does not allow you to hack the + angles. It can format a genuine Wedge object for you with its + format method. + """ + _attrMap = AttrMap( + strokeWidth = AttrMapValue(isNumber), + fillColor = AttrMapValue(isColorOrNone), + strokeColor = AttrMapValue(isColorOrNone), + strokeDashArray = AttrMapValue(isListOfNumbersOrNone), + popout = AttrMapValue(isNumber), + fontName = AttrMapValue(isString), + fontSize = AttrMapValue(isNumber), + fontColor = AttrMapValue(isColorOrNone), + labelRadius = AttrMapValue(isNumber), + label_dx = AttrMapValue(isNumber), + label_dy = AttrMapValue(isNumber), + label_angle = AttrMapValue(isNumber), + label_boxAnchor = AttrMapValue(isBoxAnchor), + label_boxStrokeColor = AttrMapValue(isColorOrNone), + label_boxStrokeWidth = AttrMapValue(isNumber), + label_boxFillColor = AttrMapValue(isColorOrNone), + label_strokeColor = AttrMapValue(isColorOrNone), + label_strokeWidth = AttrMapValue(isNumber), + label_text = AttrMapValue(isStringOrNone), + label_leading = AttrMapValue(isNumberOrNone), + label_width = AttrMapValue(isNumberOrNone), + label_maxWidth = AttrMapValue(isNumberOrNone), + label_height = AttrMapValue(isNumberOrNone), + label_textAnchor = AttrMapValue(isTextAnchor), + label_visible = AttrMapValue(isBoolean,desc="True if the label is to be drawn"), + label_topPadding = AttrMapValue(isNumber,'padding at top of box'), + label_leftPadding = AttrMapValue(isNumber,'padding at left of box'), + label_rightPadding = AttrMapValue(isNumber,'padding at right of box'), + label_bottomPadding = AttrMapValue(isNumber,'padding at bottom of box'), + label_pointer_strokeColor = AttrMapValue(isColorOrNone,desc='Color of indicator line'), + label_pointer_strokeWidth = AttrMapValue(isNumber,desc='StrokeWidth of indicator line'), + label_pointer_elbowLength = AttrMapValue(isNumber,desc='length of final indicator line segment'), + label_pointer_edgePad = AttrMapValue(isNumber,desc='pad between pointer label and box'), + label_pointer_piePad = AttrMapValue(isNumber,desc='pad between pointer label and pie'), + swatchMarker = AttrMapValue(NoneOr(isSymbol), desc="None or makeMarker('Diamond') ..."), + ) + + def __init__(self): + self.strokeWidth = 0 + self.fillColor = None + self.strokeColor = STATE_DEFAULTS["strokeColor"] + self.strokeDashArray = STATE_DEFAULTS["strokeDashArray"] + self.popout = 0 + self.fontName = STATE_DEFAULTS["fontName"] + self.fontSize = STATE_DEFAULTS["fontSize"] + self.fontColor = STATE_DEFAULTS["fillColor"] + self.labelRadius = 1.2 + self.label_dx = self.label_dy = self.label_angle = 0 + self.label_text = None + self.label_topPadding = self.label_leftPadding = self.label_rightPadding = self.label_bottomPadding = 0 + self.label_boxAnchor = 'c' + self.label_boxStrokeColor = None #boxStroke + self.label_boxStrokeWidth = 0.5 #boxStrokeWidth + self.label_boxFillColor = None + self.label_strokeColor = None + self.label_strokeWidth = 0.1 + self.label_leading = self.label_width = self.label_maxWidth = self.label_height = None + self.label_textAnchor = 'start' + self.label_visible = 1 + self.label_pointer_strokeColor = colors.black + self.label_pointer_strokeWidth = 0.5 + self.label_pointer_elbowLength = 3 + self.label_pointer_edgePad = 2 + self.label_pointer_piePad = 3 + +def _addWedgeLabel(self,text,add,angle,labelX,labelY,wedgeStyle,labelClass=WedgeLabel): + # now draw a label + if self.simpleLabels: + theLabel = String(labelX, labelY, text) + theLabel.textAnchor = "middle" + theLabel._pmv = angle + else: + theLabel = labelClass() + theLabel._pmv = angle + theLabel.x = labelX + theLabel.y = labelY + theLabel.dx = wedgeStyle.label_dx + theLabel.dy = wedgeStyle.label_dy + theLabel.angle = wedgeStyle.label_angle + theLabel.boxAnchor = wedgeStyle.label_boxAnchor + theLabel.boxStrokeColor = wedgeStyle.label_boxStrokeColor + theLabel.boxStrokeWidth = wedgeStyle.label_boxStrokeWidth + theLabel.boxFillColor = wedgeStyle.label_boxFillColor + theLabel.strokeColor = wedgeStyle.label_strokeColor + theLabel.strokeWidth = wedgeStyle.label_strokeWidth + _text = wedgeStyle.label_text + if _text is None: _text = text + theLabel._text = _text + theLabel.leading = wedgeStyle.label_leading + theLabel.width = wedgeStyle.label_width + theLabel.maxWidth = wedgeStyle.label_maxWidth + theLabel.height = wedgeStyle.label_height + theLabel.textAnchor = wedgeStyle.label_textAnchor + theLabel.visible = wedgeStyle.label_visible + theLabel.topPadding = wedgeStyle.label_topPadding + theLabel.leftPadding = wedgeStyle.label_leftPadding + theLabel.rightPadding = wedgeStyle.label_rightPadding + theLabel.bottomPadding = wedgeStyle.label_bottomPadding + theLabel.fontSize = wedgeStyle.fontSize + theLabel.fontName = wedgeStyle.fontName + theLabel.fillColor = wedgeStyle.fontColor + add(theLabel) + +def _fixLabels(labels,n): + if labels is None: + labels = [''] * n + else: + i = n-len(labels) + if i>0: labels = labels + ['']*i + return labels + +class AbstractPieChart(PlotArea): + + def makeSwatchSample(self, rowNo, x, y, width, height): + baseStyle = self.slices + 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',None)) + swatchMarker = getattr(style, 'swatchMarker', getattr(baseStyle, 'swatchMarker',None)) + if swatchMarker: + return uSymbol2Symbol(swatchMarker,x+width/2.,y+height/2.,fillColor) + return Rect(x,y,width,height,strokeWidth=strokeWidth,strokeColor=strokeColor, + strokeDashArray=strokeDashArray,fillColor=fillColor) + + def getSeriesName(self,i,default=None): + '''return series name i or default''' + try: + text = str(self.labels[i]) + except: + text = default + if not self.simpleLabels: + _text = getattr(self.slices[i],'label_text','') + if _text is not None: text = _text + return text + +def boundsOverlap(P,Q): + return not(P[0]>Q[2]-1e-2 or Q[0]>P[2]-1e-2 or P[1]>Q[3]-1e-2 or Q[1]>P[3]-1e-2) + +def _findOverlapRun(B,i,wrap): + '''find overlap run containing B[i]''' + n = len(B) + R = [i] + while 1: + i = R[-1] + j = (i+1)%n + if j in R or not boundsOverlap(B[i],B[j]): break + R.append(j) + while 1: + i = R[0] + j = (i-1)%n + if j in R or not boundsOverlap(B[i],B[j]): break + R.insert(0,j) + return R + +def findOverlapRun(B,wrap=1): + '''determine a set of overlaps in bounding boxes B or return None''' + n = len(B) + if n>1: + for i in xrange(n-1): + R = _findOverlapRun(B,i,wrap) + if len(R)>1: return R + return None + +def fixLabelOverlaps(L): + nL = len(L) + if nL<2: return + B = [l._origdata['bounds'] for l in L] + OK = 1 + RP = [] + iter = 0 + mult = 1. + + while iter<30: + R = findOverlapRun(B) + if not R: break + nR = len(R) + if nR==nL: break + if not [r for r in RP if r in R]: + mult = 1.0 + da = 0 + r0 = R[0] + rL = R[-1] + bi = B[r0] + taa = aa = _360(L[r0]._pmv) + for r in R[1:]: + b = B[r] + da = max(da,min(b[3]-bi[1],bi[3]-b[1])) + bi = b + aa += L[r]._pmv + aa = aa/float(nR) + utaa = abs(L[rL]._pmv-taa) + ntaa = _360(utaa) + da *= mult*(nR-1)/ntaa + + for r in R: + l = L[r] + orig = l._origdata + angle = l._pmv = _360(l._pmv+da*(_360(l._pmv)-aa)) + rad = angle/_180_pi + l.x = orig['cx'] + orig['rx']*cos(rad) + l.y = orig['cy'] + orig['ry']*sin(rad) + B[r] = l.getBounds() + RP = R + mult *= 1.05 + iter += 1 + +def intervalIntersection(A,B): + x,y = max(min(A),min(B)),min(max(A),max(B)) + if x>=y: return None + return x,y + +def _makeSideArcDefs(sa,direction): + sa %= 360 + if 90<=sa<270: + if direction=='clockwise': + a = (0,90,sa),(1,-90,90),(0,-360+sa,-90) + else: + a = (0,sa,270),(1,270,450),(0,450,360+sa) + else: + offs = sa>=270 and 360 or 0 + if direction=='clockwise': + a = (1,offs-90,sa),(0,offs-270,offs-90),(1,-360+sa,offs-270) + else: + a = (1,sa,offs+90),(0,offs+90,offs+270),(1,offs+270,360+sa) + return tuple([a for a in a if a[1]<a[2]]) + +def _findLargestArc(xArcs,side): + a = [a[1] for a in xArcs if a[0]==side and a[1] is not None] + if not a: return None + if len(a)>1: a.sort(lambda x,y: cmp(y[1]-y[0],x[1]-x[0])) + return a[0] + +def _fPLSide(l,width,side=None): + data = l._origdata + if side is None: + li = data['li'] + ri = data['ri'] + if li is None: + side = 1 + i = ri + elif ri is None: + side = 0 + i = li + elif li[1]-li[0]>ri[1]-ri[0]: + side = 0 + i = li + else: + side = 1 + i = ri + w = data['width'] + edgePad = data['edgePad'] + if not side: #on left + l._pmv = 180 + l.x = edgePad+w + i = data['li'] + else: + l._pmv = 0 + l.x = width - w - edgePad + i = data['ri'] + mid = data['mid'] = (i[0]+i[1])*0.5 + data['smid'] = sin(mid/_180_pi) + data['cmid'] = cos(mid/_180_pi) + data['side'] = side + return side,w + +def _fPLCF(a,b): + return cmp(b._origdata['smid'],a._origdata['smid']) + +def _arcCF(a,b): + return cmp(a[1],b[1]) + +def _fixPointerLabels(n,L,x,y,width,height,side=None): + LR = [],[] + mlr = [0,0] + for l in L: + i,w = _fPLSide(l,width,side) + LR[i].append(l) + mlr[i] = max(w,mlr[i]) + mul = 1 + G = n*[None] + mel = 0 + hh = height*0.5 + yhh = y+hh + m = max(mlr) + for i in (0,1): + T = LR[i] + if T: + B = [] + aB = B.append + S = [] + aS = S.append + T.sort(_fPLCF) + p = 0 + yh = y+height + for l in T: + data = l._origdata + inc = x+mul*(m-data['width']) + l.x += inc + G[data['index']] = l + ly = yhh+data['smid']*hh + b = data['bounds'] + b2 = (b[3]-b[1])*0.5 + if ly+b2>yh: ly = yh-b2 + if ly-b2<y: ly = y+b2 + data['bounds'] = b = (b[0],ly-b2,b[2],ly+b2) + aB(b) + l.y = ly + aS(max(0,yh-ly-b2)) + yh = ly-b2 + p = max(p,data['edgePad']+data['piePad']) + mel = max(mel,abs(data['smid']*(hh+data['elbowLength']))-hh) + aS(yh-y) + + iter = 0 + nT = len(T) + while iter<30: + R = findOverlapRun(B,wrap=0) + if not R: break + nR = len(R) + if nR==nT: break + j0 = R[0] + j1 = R[-1] + jl = j1+1 + sAbove = sum(S[:j0+1]) + sFree = sAbove+sum(S[jl:]) + sNeed = sum([b[3]-b[1] for b in B[j0:jl]])+jl-j0-(B[j0][3]-B[j1][1]) + if sNeed>sFree: break + yh = B[j0][3]+sAbove*sNeed/sFree + for r in R: + l = T[r] + data = l._origdata + b = data['bounds'] + b2 = (b[3]-b[1])*0.5 + yh -= 0.5 + ly = l.y = yh-b2 + B[r] = data['bounds'] = (b[0],ly-b2,b[2],yh) + yh = ly - b2 - 0.5 + mlr[i] = m+p + mul = -1 + return G, mlr[0], mlr[1], mel + +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 + n += 1 + style = self.slices[i%styleCount] + _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): + from operator import add + data = self.data + self._sum = 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 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] + + # 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 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: + 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 LegendedPie(Pie): + """Pie with a two part legend (one editable with swatches, one hidden without swatches).""" + + _attrMap = AttrMap(BASE=Pie, + drawLegend = AttrMapValue(isBoolean, desc="If true then create and draw legend"), + legend1 = AttrMapValue(None, desc="Handle to legend for pie"), + legendNumberFormat = AttrMapValue(None, desc="Formatting routine for number on right hand side of legend."), + legendNumberOffset = AttrMapValue(isNumber, desc="Horizontal space between legend and numbers on r/hand side"), + pieAndLegend_colors = AttrMapValue(isListOfColors, desc="Colours used for both swatches and pie"), + legend_names = AttrMapValue(isNoneOrListOfNoneOrStrings, desc="Names used in legend (or None)"), + legend_data = AttrMapValue(isNoneOrListOfNoneOrNumbers, desc="Numbers used on r/hand side of legend (or None)"), + leftPadding = AttrMapValue(isNumber, desc='Padding on left of drawing'), + rightPadding = AttrMapValue(isNumber, desc='Padding on right of drawing'), + topPadding = AttrMapValue(isNumber, desc='Padding at top of drawing'), + bottomPadding = AttrMapValue(isNumber, desc='Padding at bottom of drawing'), + ) + + def __init__(self): + Pie.__init__(self) + self.x = 0 + self.y = 0 + self.height = 100 + self.width = 100 + self.data = [38.4, 20.7, 18.9, 15.4, 6.6] + self.labels = None + self.direction = 'clockwise' + PCMYKColor, black = colors.PCMYKColor, colors.black + self.pieAndLegend_colors = [PCMYKColor(11,11,72,0,spotName='PANTONE 458 CV'), + PCMYKColor(100,65,0,30,spotName='PANTONE 288 CV'), + PCMYKColor(11,11,72,0,spotName='PANTONE 458 CV',density=75), + PCMYKColor(100,65,0,30,spotName='PANTONE 288 CV',density=75), + PCMYKColor(11,11,72,0,spotName='PANTONE 458 CV',density=50), + PCMYKColor(100,65,0,30,spotName='PANTONE 288 CV',density=50)] + + #Allows us up to six 'wedges' to be coloured + self.slices[0].fillColor=self.pieAndLegend_colors[0] + self.slices[1].fillColor=self.pieAndLegend_colors[1] + self.slices[2].fillColor=self.pieAndLegend_colors[2] + self.slices[3].fillColor=self.pieAndLegend_colors[3] + self.slices[4].fillColor=self.pieAndLegend_colors[4] + self.slices[5].fillColor=self.pieAndLegend_colors[5] + + self.slices.strokeWidth = 0.75 + self.slices.strokeColor = black + + legendOffset = 17 + self.legendNumberOffset = 51 + self.legendNumberFormat = '%.1f%%' + self.legend_data = self.data + + #set up the legends + from reportlab.graphics.charts.legends import Legend + self.legend1 = Legend() + self.legend1.x = self.width+legendOffset + self.legend1.y = self.height + self.legend1.deltax = 5.67 + self.legend1.deltay = 14.17 + self.legend1.dxTextSpace = 11.39 + self.legend1.dx = 5.67 + self.legend1.dy = 5.67 + self.legend1.columnMaximum = 7 + self.legend1.alignment = 'right' + self.legend_names = ['AAA:','AA:','A:','BBB:','NR:'] + for f in range(0,len(self.data)): + self.legend1.colorNamePairs.append((self.pieAndLegend_colors[f], self.legend_names[f])) + self.legend1.fontName = "Helvetica-Bold" + self.legend1.fontSize = 6 + self.legend1.strokeColor = black + self.legend1.strokeWidth = 0.5 + + self._legend2 = Legend() + self._legend2.dxTextSpace = 0 + self._legend2.dx = 0 + self._legend2.alignment = 'right' + self._legend2.fontName = "Helvetica-Oblique" + self._legend2.fontSize = 6 + self._legend2.strokeColor = self.legend1.strokeColor + + self.leftPadding = 5 + self.rightPadding = 5 + self.topPadding = 5 + self.bottomPadding = 5 + self.drawLegend = 1 + + def draw(self): + if self.drawLegend: + self.legend1.colorNamePairs = [] + self._legend2.colorNamePairs = [] + for f in range(0,len(self.data)): + if self.legend_names == None: + self.slices[f].fillColor = self.pieAndLegend_colors[f] + self.legend1.colorNamePairs.append((self.pieAndLegend_colors[f], None)) + else: + try: + self.slices[f].fillColor = self.pieAndLegend_colors[f] + self.legend1.colorNamePairs.append((self.pieAndLegend_colors[f], self.legend_names[f])) + except IndexError: + self.slices[f].fillColor = self.pieAndLegend_colors[f%len(self.pieAndLegend_colors)] + self.legend1.colorNamePairs.append((self.pieAndLegend_colors[f%len(self.pieAndLegend_colors)], self.legend_names[f])) + if self.legend_data != None: + ldf = self.legend_data[f] + lNF = self.legendNumberFormat + from types import StringType + if ldf is None or lNF is None: + pass + elif type(lNF) is StringType: + ldf = lNF % ldf + elif callable(lNF): + ldf = lNF(ldf) + else: + p = self.legend_names[f] + if self.legend_data != None: + ldf = self.legend_data[f] + lNF = self.legendNumberFormat + if ldf is None or lNF is None: + pass + elif type(lNF) is StringType: + ldf = lNF % ldf + elif callable(lNF): + ldf = lNF(ldf) + else: + msg = "Unknown formatter type %s, expected string or function" % self.legendNumberFormat + raise Exception, msg + self._legend2.colorNamePairs.append((None,ldf)) + p = Pie.draw(self) + if self.drawLegend: + p.add(self.legend1) + #hide from user - keeps both sides lined up! + self._legend2.x = self.legend1.x+self.legendNumberOffset + self._legend2.y = self.legend1.y + self._legend2.deltax = self.legend1.deltax + self._legend2.deltay = self.legend1.deltay + self._legend2.dy = self.legend1.dy + self._legend2.columnMaximum = self.legend1.columnMaximum + p.add(self._legend2) + p.shift(self.leftPadding, self.bottomPadding) + return p + + def _getDrawingDimensions(self): + tx = self.rightPadding + if self.drawLegend: + tx = tx+self.legend1.x+self.legendNumberOffset #self._legend2.x + tx = tx + self._legend2._calculateMaxWidth(self._legend2.colorNamePairs) + ty = self.bottomPadding+self.height+self.topPadding + return (tx,ty) + + def demo(self, drawing=None): + if not drawing: + tx,ty = self._getDrawingDimensions() + drawing = Drawing(tx, ty) + drawing.add(self.draw()) + return drawing + +from utils3d import _getShaded, _2rad, _360, _pi_2, _2pi, _180_pi +class Wedge3dProperties(PropHolder): + """This holds descriptive information about the wedges in a pie chart. + + It is not to be confused with the 'wedge itself'; this just holds + a recipe for how to format one, and does not allow you to hack the + angles. It can format a genuine Wedge object for you with its + format method. + """ + _attrMap = AttrMap( + fillColor = AttrMapValue(isColorOrNone), + fillColorShaded = AttrMapValue(isColorOrNone), + fontColor = AttrMapValue(isColorOrNone), + fontName = AttrMapValue(isString), + fontSize = AttrMapValue(isNumber), + label_angle = AttrMapValue(isNumber), + label_bottomPadding = AttrMapValue(isNumber,'padding at bottom of box'), + label_boxAnchor = AttrMapValue(isBoxAnchor), + label_boxFillColor = AttrMapValue(isColorOrNone), + label_boxStrokeColor = AttrMapValue(isColorOrNone), + label_boxStrokeWidth = AttrMapValue(isNumber), + label_dx = AttrMapValue(isNumber), + label_dy = AttrMapValue(isNumber), + label_height = AttrMapValue(isNumberOrNone), + label_leading = AttrMapValue(isNumberOrNone), + label_leftPadding = AttrMapValue(isNumber,'padding at left of box'), + label_maxWidth = AttrMapValue(isNumberOrNone), + label_rightPadding = AttrMapValue(isNumber,'padding at right of box'), + label_strokeColor = AttrMapValue(isColorOrNone), + label_strokeWidth = AttrMapValue(isNumber), + label_text = AttrMapValue(isStringOrNone), + label_textAnchor = AttrMapValue(isTextAnchor), + label_topPadding = AttrMapValue(isNumber,'padding at top of box'), + label_visible = AttrMapValue(isBoolean,desc="True if the label is to be drawn"), + label_width = AttrMapValue(isNumberOrNone), + labelRadius = AttrMapValue(isNumber), + popout = AttrMapValue(isNumber), + shading = AttrMapValue(isNumber), + strokeColor = AttrMapValue(isColorOrNone), + strokeColorShaded = AttrMapValue(isColorOrNone), + strokeDashArray = AttrMapValue(isListOfNumbersOrNone), + strokeWidth = AttrMapValue(isNumber), + visible = AttrMapValue(isBoolean,'set to false to skip displaying'), + ) + + def __init__(self): + self.strokeWidth = 0 + self.shading = 0.3 + self.visible = 1 + self.strokeColorShaded = self.fillColorShaded = self.fillColor = None + self.strokeColor = STATE_DEFAULTS["strokeColor"] + self.strokeDashArray = STATE_DEFAULTS["strokeDashArray"] + self.popout = 0 + self.fontName = STATE_DEFAULTS["fontName"] + self.fontSize = STATE_DEFAULTS["fontSize"] + self.fontColor = STATE_DEFAULTS["fillColor"] + self.labelRadius = 1.2 + self.label_dx = self.label_dy = self.label_angle = 0 + self.label_text = None + self.label_topPadding = self.label_leftPadding = self.label_rightPadding = self.label_bottomPadding = 0 + self.label_boxAnchor = 'c' + self.label_boxStrokeColor = None #boxStroke + self.label_boxStrokeWidth = 0.5 #boxStrokeWidth + self.label_boxFillColor = None + self.label_strokeColor = None + self.label_strokeWidth = 0.1 + self.label_leading = self.label_width = self.label_maxWidth = self.label_height = None + self.label_textAnchor = 'start' + self.label_visible = 1 + +class _SL3D: + def __init__(self,lo,hi): + if lo<0: + lo += 360 + hi += 360 + self.lo = lo + self.hi = hi + self.mid = (lo+hi)*0.5 + + def __str__(self): + return '_SL3D(%.2f,%.2f)' % (self.lo,self.hi) + +_270r = _2rad(270) +class Pie3d(Pie): + _attrMap = AttrMap(BASE=Pie, + perspective = AttrMapValue(isNumber, desc='A flattening parameter.'), + depth_3d = AttrMapValue(isNumber, desc='depth of the pie.'), + angle_3d = AttrMapValue(isNumber, desc='The view angle.'), + ) + perspective = 70 + depth_3d = 25 + angle_3d = 180 + + def _popout(self,i): + return self.slices[i].popout or 0 + + def CX(self, i,d ): + return self._cx+(d and self._xdepth_3d or 0)+self._popout(i)*cos(_2rad(self._sl3d[i].mid)) + def CY(self,i,d): + return self._cy+(d and self._ydepth_3d or 0)+self._popout(i)*sin(_2rad(self._sl3d[i].mid)) + def OX(self,i,o,d): + return self.CX(i,d)+self._radiusx*cos(_2rad(o)) + def OY(self,i,o,d): + return self.CY(i,d)+self._radiusy*sin(_2rad(o)) + + def rad_dist(self,a): + _3dva = self._3dva + return min(abs(a-_3dva),abs(a-_3dva+360)) + + def __init__(self): + self.x = 0 + self.y = 0 + self.width = 300 + self.height = 200 + self.data = [12.50,20.10,2.00,22.00,5.00,18.00,13.00] + self.labels = None # or list of strings + self.startAngle = 90 + self.direction = "clockwise" + self.simpleLabels = 1 + self.slices = TypedPropertyCollection(Wedge3dProperties) + 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.azure + self.slices[5].fillColor = colors.crimson + self.slices[6].fillColor = colors.darkviolet + self.checkLabelOverlap = 0 + + def _fillSide(self,L,i,angle,strokeColor,strokeWidth,fillColor): + rd = self.rad_dist(angle) + if rd<self.rad_dist(self._sl3d[i].mid): + p = [self.CX(i,0),self.CY(i,0), + self.CX(i,1),self.CY(i,1), + self.OX(i,angle,1),self.OY(i,angle,1), + self.OX(i,angle,0),self.OY(i,angle,0)] + L.append((rd,Polygon(p, strokeColor=strokeColor, fillColor=fillColor,strokeWidth=strokeWidth,strokeLineJoin=1))) + + def draw(self): + slices = self.slices + _3d_angle = self.angle_3d + _3dva = self._3dva = _360(_3d_angle+90) + a0 = _2rad(_3dva) + self._xdepth_3d = cos(a0)*self.depth_3d + self._ydepth_3d = sin(a0)*self.depth_3d + self._cx = self.x+self.width/2.0 + self._cy = self.y+(self.height - self._ydepth_3d)/2.0 + radius = self._radius = self._cx-self.x + self._radiusx = radiusx = radius + self._radiusy = radiusy = (1.0 - self.perspective/100.0)*radius + data = self.normalizeData() + sum = self._sum + + CX = self.CX + CY = self.CY + OX = self.OX + OY = self.OY + rad_dist = self.rad_dist + _fillSide = self._fillSide + self._seriesCount = n = len(data) + _sl3d = self._sl3d = [] + g = Group() + last = _360(self.startAngle) + a0 = self.direction=='clockwise' and -1 or 1 + for v in data: + v *= a0 + angle1, angle0 = last, v+last + last = angle0 + if a0>0: angle0, angle1 = angle1, angle0 + _sl3d.append(_SL3D(angle0,angle1)) + + labels = _fixLabels(self.labels,n) + a0 = _3d_angle + a1 = _3d_angle+180 + T = [] + S = [] + L = [] + + class WedgeLabel3d(WedgeLabel): + _ydepth_3d = self._ydepth_3d + def _checkDXY(self,ba): + if ba[0]=='n': + if not hasattr(self,'_ody'): + self._ody = self.dy + self.dy = -self._ody + self._ydepth_3d + + checkLabelOverlap = self.checkLabelOverlap + + for i in xrange(n): + style = slices[i] + if not style.visible: continue + sl = _sl3d[i] + lo = angle0 = sl.lo + hi = angle1 = sl.hi + if abs(hi-lo)<=1e-7: continue + fillColor = _getShaded(style.fillColor,style.fillColorShaded,style.shading) + strokeColor = _getShaded(style.strokeColor,style.strokeColorShaded,style.shading) or fillColor + strokeWidth = style.strokeWidth + cx0 = CX(i,0) + cy0 = CY(i,0) + cx1 = CX(i,1) + cy1 = CY(i,1) + #background shaded pie bottom + g.add(Wedge(cx1,cy1,radiusx, lo, hi,yradius=radiusy, + strokeColor=strokeColor,strokeWidth=strokeWidth,fillColor=fillColor, + strokeLineJoin=1)) + #connect to top + if lo < a0 < hi: angle0 = a0 + if lo < a1 < hi: angle1 = a1 + if 1: + p = ArcPath(strokeColor=strokeColor, fillColor=fillColor,strokeWidth=strokeWidth,strokeLineJoin=1) + p.addArc(cx1,cy1,radiusx,angle0,angle1,yradius=radiusy,moveTo=1) + p.lineTo(OX(i,angle1,0),OY(i,angle1,0)) + p.addArc(cx0,cy0,radiusx,angle0,angle1,yradius=radiusy,reverse=1) + p.closePath() + if angle0<=_3dva and angle1>=_3dva: + rd = 0 + else: + rd = min(rad_dist(angle0),rad_dist(angle1)) + S.append((rd,p)) + _fillSide(S,i,lo,strokeColor,strokeWidth,fillColor) + _fillSide(S,i,hi,strokeColor,strokeWidth,fillColor) + + #bright shaded top + fillColor = style.fillColor + strokeColor = style.strokeColor or fillColor + T.append(Wedge(cx0,cy0,radiusx,lo,hi,yradius=radiusy, + strokeColor=strokeColor,strokeWidth=strokeWidth,fillColor=fillColor,strokeLineJoin=1)) + + text = labels[i] + if text: + rat = style.labelRadius + self._radiusx *= rat + self._radiusy *= rat + mid = sl.mid + labelX = OX(i,mid,0) + labelY = OY(i,mid,0) + _addWedgeLabel(self,text,L.append,mid,labelX,labelY,style,labelClass=WedgeLabel3d) + if checkLabelOverlap: + l = L[-1] + l._origdata = { 'x': labelX, 'y':labelY, 'angle': mid, + 'rx': self._radiusx, 'ry':self._radiusy, 'cx':CX(i,0), 'cy':CY(i,0), + 'bounds': l.getBounds(), + } + self._radiusx = radiusx + self._radiusy = radiusy + + S.sort(lambda a,b: -cmp(a[0],b[0])) + if checkLabelOverlap: + fixLabelOverlaps(L) + map(g.add,map(lambda x:x[1],S)+T+L) + return g + + 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 + self.slices[1].visible = 0 + self.slices[3].visible = 1 + self.slices[4].visible = 1 + self.slices[5].visible = 1 + self.slices[6].visible = 0 + + d.add(pc) + return d + + +def sample0a(): + "Make a degenerated pie chart with only one slice." + + d = Drawing(400, 200) + + pc = Pie() + pc.x = 150 + pc.y = 50 + pc.data = [10] + pc.labels = ['a'] + pc.slices.strokeWidth=1#0.5 + + d.add(pc) + + return d + + +def sample0b(): + "Make a degenerated pie chart with only one slice." + + d = Drawing(400, 200) + + pc = Pie() + pc.x = 150 + pc.y = 50 + pc.width = 120 + pc.height = 100 + pc.data = [10] + pc.labels = ['a'] + pc.slices.strokeWidth=1#0.5 + + d.add(pc) + + return d + + +def sample1(): + "Make a typical pie chart with with one slice treated in a special way." + + d = Drawing(400, 200) + + pc = Pie() + pc.x = 150 + pc.y = 50 + pc.data = [10, 20, 30, 40, 50, 60] + pc.labels = ['a', 'b', 'c', 'd', 'e', 'f'] + + pc.slices.strokeWidth=1#0.5 + pc.slices[3].popout = 20 + pc.slices[3].strokeWidth = 2 + pc.slices[3].strokeDashArray = [2,2] + pc.slices[3].labelRadius = 1.75 + pc.slices[3].fontColor = colors.red + + d.add(pc) + + return d + + +def sample2(): + "Make a pie chart with nine slices." + + d = Drawing(400, 200) + + pc = Pie() + pc.x = 125 + pc.y = 25 + pc.data = [0.31, 0.148, 0.108, + 0.076, 0.033, 0.03, + 0.019, 0.126, 0.15] + pc.labels = ['1', '2', '3', '4', '5', '6', '7', '8', 'X'] + + pc.width = 150 + pc.height = 150 + pc.slices.strokeWidth=1#0.5 + + pc.slices[0].fillColor = colors.steelblue + pc.slices[1].fillColor = colors.thistle + pc.slices[2].fillColor = colors.cornflower + pc.slices[3].fillColor = colors.lightsteelblue + pc.slices[4].fillColor = colors.aquamarine + pc.slices[5].fillColor = colors.cadetblue + pc.slices[6].fillColor = colors.lightcoral + pc.slices[7].fillColor = colors.tan + pc.slices[8].fillColor = colors.darkseagreen + + d.add(pc) + + return d + + +def sample3(): + "Make a pie chart with a very slim slice." + + d = Drawing(400, 200) + + pc = Pie() + pc.x = 125 + pc.y = 25 + + pc.data = [74, 1, 25] + + pc.width = 150 + pc.height = 150 + pc.slices.strokeWidth=1#0.5 + pc.slices[0].fillColor = colors.steelblue + pc.slices[1].fillColor = colors.thistle + pc.slices[2].fillColor = colors.cornflower + + d.add(pc) + + return d + + +def sample4(): + "Make a pie chart with several very slim slices." + + d = Drawing(400, 200) + + pc = Pie() + pc.x = 125 + pc.y = 25 + + pc.data = [74, 1, 1, 1, 1, 22] + + pc.width = 150 + pc.height = 150 + pc.slices.strokeWidth=1#0.5 + pc.slices[0].fillColor = colors.steelblue + pc.slices[1].fillColor = colors.thistle + pc.slices[2].fillColor = colors.cornflower + pc.slices[3].fillColor = colors.lightsteelblue + pc.slices[4].fillColor = colors.aquamarine + pc.slices[5].fillColor = colors.cadetblue + + d.add(pc) + + return d diff --git a/bin/reportlab/graphics/charts/slidebox.py b/bin/reportlab/graphics/charts/slidebox.py new file mode 100644 index 00000000000..309fb07ba0d --- /dev/null +++ b/bin/reportlab/graphics/charts/slidebox.py @@ -0,0 +1,186 @@ +from reportlab.lib.colors import Color, white, black +from reportlab.graphics.charts.textlabels import Label +from reportlab.graphics.shapes import Polygon, Line, Circle, String, Drawing, PolyLine, Group, Rect +from reportlab.graphics.widgetbase import Widget, TypedPropertyCollection +from reportlab.lib.attrmap import * +from reportlab.lib.validators import * +from reportlab.lib.units import cm +from reportlab.pdfbase.pdfmetrics import stringWidth, getFont +from reportlab.graphics.widgets.grids import ShadedRect, Grid + +class SlideBox(Widget): + """Returns a slidebox widget""" + _attrMap = AttrMap( + labelFontName = AttrMapValue(isString, desc="Name of font used for the labels"), + labelFontSize = AttrMapValue(isNumber, desc="Size of font used for the labels"), + labelStrokeColor = AttrMapValue(isColorOrNone, desc="Colour for for number outlines"), + labelFillColor = AttrMapValue(isColorOrNone, desc="Colour for number insides"), + startColor = AttrMapValue(isColor, desc='Color of first box'), + endColor = AttrMapValue(isColor, desc='Color of last box'), + numberOfBoxes = AttrMapValue(isInt, desc='How many boxes there are'), + trianglePosition = AttrMapValue(isInt, desc='Which box is highlighted by the triangles'), + triangleHeight = AttrMapValue(isNumber, desc="Height of indicator triangles"), + triangleWidth = AttrMapValue(isNumber, desc="Width of indicator triangles"), + triangleFillColor = AttrMapValue(isColor, desc="Colour of indicator triangles"), + triangleStrokeColor = AttrMapValue(isColorOrNone, desc="Colour of indicator triangle outline"), + triangleStrokeWidth = AttrMapValue(isNumber, desc="Colour of indicator triangle outline"), + boxHeight = AttrMapValue(isNumber, desc="Height of the boxes"), + boxWidth = AttrMapValue(isNumber, desc="Width of the boxes"), + boxSpacing = AttrMapValue(isNumber, desc="Space between the boxes"), + boxOutlineColor = AttrMapValue(isColorOrNone, desc="Colour used to outline the boxes (if any)"), + boxOutlineWidth = AttrMapValue(isNumberOrNone, desc="Width of the box outline (if any)"), + leftPadding = AttrMapValue(isNumber, desc='Padding on left of drawing'), + rightPadding = AttrMapValue(isNumber, desc='Padding on right of drawing'), + topPadding = AttrMapValue(isNumber, desc='Padding at top of drawing'), + bottomPadding = AttrMapValue(isNumber, desc='Padding at bottom of drawing'), + background = AttrMapValue(isColorOrNone, desc='Colour of the background to the drawing (if any)'), + sourceLabelText = AttrMapValue(isNoneOrString, desc="Text used for the 'source' label (can be empty)"), + sourceLabelOffset = AttrMapValue(isNumber, desc='Padding at bottom of drawing'), + sourceLabelFontName = AttrMapValue(isString, desc="Name of font used for the 'source' label"), + sourceLabelFontSize = AttrMapValue(isNumber, desc="Font size for the 'source' label"), + sourceLabelFillColor = AttrMapValue(isColorOrNone, desc="Colour ink for the 'source' label (bottom right)"), + ) + + def __init__(self): + self.labelFontName = "Helvetica-Bold" + self.labelFontSize = 10 + self.labelStrokeColor = black + self.labelFillColor = white + self.startColor = colors.Color(232/255.0,224/255.0,119/255.0) + self.endColor = colors.Color(25/255.0,77/255.0,135/255.0) + self.numberOfBoxes = 7 + self.trianglePosition = 7 + self.triangleHeight = 0.12*cm + self.triangleWidth = 0.38*cm + self.triangleFillColor = white + self.triangleStrokeColor = black + self.triangleStrokeWidth = 0.58 + self.boxHeight = 0.55*cm + self.boxWidth = 0.73*cm + self.boxSpacing = 0.075*cm + self.boxOutlineColor = black + self.boxOutlineWidth = 0.58 + self.leftPadding=5 + self.rightPadding=5 + self.topPadding=5 + self.bottomPadding=5 + self.background=None + self.sourceLabelText = "Source: ReportLab" + self.sourceLabelOffset = 0.2*cm + self.sourceLabelFontName = "Helvetica-Oblique" + self.sourceLabelFontSize = 6 + self.sourceLabelFillColor = black + + def _getDrawingDimensions(self): + tx=(self.numberOfBoxes*self.boxWidth) + if self.numberOfBoxes>1: tx=tx+((self.numberOfBoxes-1)*self.boxSpacing) + tx=tx+self.leftPadding+self.rightPadding + ty=self.boxHeight+self.triangleHeight + ty=ty+self.topPadding+self.bottomPadding+self.sourceLabelOffset+self.sourceLabelFontSize + return (tx,ty) + + def _getColors(self): + # for calculating intermediate colors... + numShades = self.numberOfBoxes+1 + fillColorStart = self.startColor + fillColorEnd = self.endColor + colorsList =[] + + for i in range(0,numShades): + colorsList.append(colors.linearlyInterpolatedColor(fillColorStart, fillColorEnd, 0, numShades-1, i)) + return colorsList + + def demo(self,drawing=None): + from reportlab.lib import colors + if not drawing: + tx,ty=self._getDrawingDimensions() + drawing = Drawing(tx,ty) + drawing.add(self.draw()) + return drawing + + def draw(self): + g = Group() + ys = self.bottomPadding+(self.triangleHeight/2)+self.sourceLabelOffset+self.sourceLabelFontSize + if self.background: + x,y = self._getDrawingDimensions() + g.add(Rect(-self.leftPadding,-ys,x,y, + strokeColor=None, + strokeWidth=0, + fillColor=self.background)) + + ascent=getFont(self.labelFontName).face.ascent/1000. + if ascent==0: ascent=0.718 # default (from helvetica) + ascent=ascent*self.labelFontSize # normalize + + colorsList = self._getColors() + + # Draw the boxes - now uses ShadedRect from grids + x=0 + for f in range (0,self.numberOfBoxes): + sr=ShadedRect() + sr.x=x + sr.y=0 + sr.width=self.boxWidth + sr.height=self.boxHeight + sr.orientation = 'vertical' + sr.numShades = 30 + sr.fillColorStart = colorsList[f] + sr.fillColorEnd = colorsList[f+1] + sr.strokeColor = None + sr.strokeWidth = 0 + + g.add(sr) + + g.add(Rect(x,0,self.boxWidth,self.boxHeight, + strokeColor=self.boxOutlineColor, + strokeWidth=self.boxOutlineWidth, + fillColor=None)) + + g.add(String(x+self.boxWidth/2.,(self.boxHeight-ascent)/2., + text = str(f+1), + fillColor = self.labelFillColor, + strokeColor=self.labelStrokeColor, + textAnchor = 'middle', + fontName = self.labelFontName, + fontSize = self.labelFontSize)) + x=x+self.boxWidth+self.boxSpacing + + #do triangles + xt = (self.trianglePosition*self.boxWidth) + if self.trianglePosition>1: + xt = xt+(self.trianglePosition-1)*self.boxSpacing + xt = xt-(self.boxWidth/2) + g.add(Polygon( + strokeColor = self.triangleStrokeColor, + strokeWidth = self.triangleStrokeWidth, + fillColor = self.triangleFillColor, + points=[xt,self.boxHeight-(self.triangleHeight/2), + xt-(self.triangleWidth/2),self.boxHeight+(self.triangleHeight/2), + xt+(self.triangleWidth/2),self.boxHeight+(self.triangleHeight/2), + xt,self.boxHeight-(self.triangleHeight/2)])) + g.add(Polygon( + strokeColor = self.triangleStrokeColor, + strokeWidth = self.triangleStrokeWidth, + fillColor = self.triangleFillColor, + points=[xt,0+(self.triangleHeight/2), + xt-(self.triangleWidth/2),0-(self.triangleHeight/2), + xt+(self.triangleWidth/2),0-(self.triangleHeight/2), + xt,0+(self.triangleHeight/2)])) + + #source label + if self.sourceLabelText != None: + g.add(String(x-self.boxSpacing,0-(self.triangleHeight/2)-self.sourceLabelOffset-(self.sourceLabelFontSize), + text = self.sourceLabelText, + fillColor = self.sourceLabelFillColor, + textAnchor = 'end', + fontName = self.sourceLabelFontName, + fontSize = self.sourceLabelFontSize)) + + g.shift(self.leftPadding, ys) + + return g + + +if __name__ == "__main__": + d = SlideBox() + d.demo().save(fnRoot="slidebox") diff --git a/bin/reportlab/graphics/charts/spider.py b/bin/reportlab/graphics/charts/spider.py new file mode 100644 index 00000000000..6de92724390 --- /dev/null +++ b/bin/reportlab/graphics/charts/spider.py @@ -0,0 +1,407 @@ + #Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/spider.py +# spider chart, also known as radar chart + +"""Spider Chart + +Normal use shows variation of 5-10 parameters against some 'norm' or target. +When there is more than one series, place the series with the largest +numbers first, as it will be overdrawn by each successive one. +""" +__version__=''' $Id: spider.py 2676 2005-09-06 10:25:00Z rgbecker $ ''' + +import copy +from math import sin, cos, pi + +from reportlab.lib import colors +from reportlab.lib.validators import isColor, isNumber, isListOfNumbersOrNone,\ + isListOfNumbers, isColorOrNone, isString,\ + isListOfStringsOrNone, OneOf, SequenceOf,\ + isBoolean, isListOfColors, isNumberOrNone,\ + isNoneOrListOfNoneOrStrings, isTextAnchor,\ + isNoneOrListOfNoneOrNumbers, isBoxAnchor,\ + isStringOrNone, isStringOrNone, EitherOr,\ + isCallable +from reportlab.lib.attrmap import * +from reportlab.pdfgen.canvas import Canvas +from reportlab.graphics.shapes import Group, Drawing, Line, Rect, Polygon, PolyLine, Ellipse, \ + Wedge, String, STATE_DEFAULTS +from reportlab.graphics.widgetbase import Widget, TypedPropertyCollection, PropHolder +from reportlab.graphics.charts.areas import PlotArea +from piecharts import WedgeLabel +from reportlab.graphics.widgets.markers import makeMarker, uSymbol2Symbol, isSymbol + +class StrandProperty(PropHolder): + + _attrMap = AttrMap( + strokeWidth = AttrMapValue(isNumber), + fillColor = AttrMapValue(isColorOrNone), + strokeColor = AttrMapValue(isColorOrNone), + strokeDashArray = AttrMapValue(isListOfNumbersOrNone), + symbol = AttrMapValue(EitherOr((isStringOrNone,isSymbol)), desc='Widget placed at data points.'), + symbolSize= AttrMapValue(isNumber, desc='Symbol size.'), + name = AttrMapValue(isStringOrNone, desc='Name of the strand.'), + ) + + def __init__(self): + self.strokeWidth = 1 + self.fillColor = None + self.strokeColor = STATE_DEFAULTS["strokeColor"] + self.strokeDashArray = STATE_DEFAULTS["strokeDashArray"] + self.symbol = None + self.symbolSize = 5 + self.name = None + +class SpokeProperty(PropHolder): + _attrMap = AttrMap( + strokeWidth = AttrMapValue(isNumber), + fillColor = AttrMapValue(isColorOrNone), + strokeColor = AttrMapValue(isColorOrNone), + strokeDashArray = AttrMapValue(isListOfNumbersOrNone), + labelRadius = AttrMapValue(isNumber), + visible = AttrMapValue(isBoolean,desc="True if the spoke line is to be drawn"), + ) + + def __init__(self,**kw): + self.strokeWidth = 0.5 + self.fillColor = None + self.strokeColor = STATE_DEFAULTS["strokeColor"] + self.strokeDashArray = STATE_DEFAULTS["strokeDashArray"] + self.visible = 1 + self.labelRadius = 1.05 + +class SpokeLabel(WedgeLabel): + def __init__(self,**kw): + WedgeLabel.__init__(self,**kw) + if '_text' not in kw.keys(): self._text = '' + +class StrandLabel(SpokeLabel): + _attrMap = AttrMap(BASE=SpokeLabel, + format = AttrMapValue(EitherOr((isStringOrNone,isCallable)),"Format for the label"), + dR = AttrMapValue(isNumberOrNone,"radial shift for label"), + ) + def __init__(self,**kw): + self.format = '' + self.dR = 0 + SpokeLabel.__init__(self,**kw) + +def _setupLabel(labelClass, text, radius, cx, cy, angle, car, sar, sty): + L = labelClass() + L._text = text + L.x = cx + radius*car + L.y = cy + radius*sar + L._pmv = angle*180/pi + L.boxAnchor = sty.boxAnchor + L.dx = sty.dx + L.dy = sty.dy + L.angle = sty.angle + L.boxAnchor = sty.boxAnchor + L.boxStrokeColor = sty.boxStrokeColor + L.boxStrokeWidth = sty.boxStrokeWidth + L.boxFillColor = sty.boxFillColor + L.strokeColor = sty.strokeColor + L.strokeWidth = sty.strokeWidth + L.leading = sty.leading + L.width = sty.width + L.maxWidth = sty.maxWidth + L.height = sty.height + L.textAnchor = sty.textAnchor + L.visible = sty.visible + L.topPadding = sty.topPadding + L.leftPadding = sty.leftPadding + L.rightPadding = sty.rightPadding + L.bottomPadding = sty.bottomPadding + L.fontName = sty.fontName + L.fontSize = sty.fontSize + L.fillColor = sty.fillColor + return L + +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 + +def sample1(): + "Make a simple spider chart" + d = Drawing(400, 400) + sp = SpiderChart() + sp.x = 50 + sp.y = 50 + sp.width = 300 + sp.height = 300 + sp.data = [[10,12,14,16,14,12], [6,8,10,12,9,15],[7,8,17,4,12,8]] + sp.labels = ['a','b','c','d','e','f'] + sp.strands[0].strokeColor = colors.cornsilk + sp.strands[1].strokeColor = colors.cyan + sp.strands[2].strokeColor = colors.palegreen + sp.strands[0].fillColor = colors.cornsilk + sp.strands[1].fillColor = colors.cyan + sp.strands[2].fillColor = colors.palegreen + sp.spokes.strokeDashArray = (2,2) + d.add(sp) + return d + + +def sample2(): + "Make a spider chart with markers, but no fill" + d = Drawing(400, 400) + sp = SpiderChart() + sp.x = 50 + sp.y = 50 + sp.width = 300 + sp.height = 300 + sp.data = [[10,12,14,16,14,12], [6,8,10,12,9,15],[7,8,17,4,12,8]] + sp.labels = ['U','V','W','X','Y','Z'] + sp.strands.strokeWidth = 1 + sp.strands[0].fillColor = colors.pink + sp.strands[1].fillColor = colors.lightblue + sp.strands[2].fillColor = colors.palegreen + sp.strands[0].strokeColor = colors.red + sp.strands[1].strokeColor = colors.blue + sp.strands[2].strokeColor = colors.green + sp.strands.symbol = "FilledDiamond" + sp.strands[1].symbol = makeMarker("Circle") + sp.strands[1].symbol.strokeWidth = 0.5 + sp.strands[1].symbol.fillColor = colors.yellow + sp.strands.symbolSize = 6 + sp.strandLabels[0,3]._text = 'special' + sp.strandLabels[0,1]._text = 'one' + sp.strandLabels[0,0]._text = 'zero' + sp.strandLabels[1,0]._text = 'Earth' + sp.strandLabels[2,2]._text = 'Mars' + sp.strandLabels.format = 'values' + sp.strandLabels.dR = -5 + d.add(sp) + return d + + +if __name__=='__main__': + d = sample1() + from reportlab.graphics.renderPDF import drawToFile + drawToFile(d, 'spider.pdf') + d = sample2() + drawToFile(d, 'spider2.pdf') diff --git a/bin/reportlab/graphics/charts/textlabels.py b/bin/reportlab/graphics/charts/textlabels.py new file mode 100644 index 00000000000..5bcd0250e79 --- /dev/null +++ b/bin/reportlab/graphics/charts/textlabels.py @@ -0,0 +1,442 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/textlabels.py +__version__=''' $Id: textlabels.py 2647 2005-07-26 13:47:51Z rgbecker $ ''' +import string + +from reportlab.lib import colors +from reportlab.lib.validators import isNumber, isNumberOrNone, OneOf, isColorOrNone, isString, \ + isTextAnchor, isBoxAnchor, isBoolean, NoneOr, isInstanceOf, isNoneOrString +from reportlab.lib.attrmap import * +from reportlab.pdfbase.pdfmetrics import stringWidth +from reportlab.graphics.shapes import Drawing, Group, Circle, Rect, String, STATE_DEFAULTS +from reportlab.graphics.shapes import _PATH_OP_ARG_COUNT, _PATH_OP_NAMES, definePath +from reportlab.graphics.widgetbase import Widget, PropHolder + +_gs = None +_A2BA= { + 'x': {0:'n', 45:'ne', 90:'e', 135:'se', 180:'s', 225:'sw', 270:'w', 315: 'nw', -45: 'nw'}, + 'y': {0:'e', 45:'se', 90:'s', 135:'sw', 180:'w', 225:'nw', 270:'n', 315: 'ne', -45: 'ne'}, + } +def _simpleSplit(txt,mW,SW): + L = [] + ws = SW(' ') + O = [] + w = -ws + for t in string.split(txt): + lt = SW(t) + if w+ws+lt<=mW or O==[]: + O.append(t) + w = w + ws + lt + else: + L.append(string.join(O,' ')) + O = [t] + w = lt + if O!=[]: L.append(string.join(O,' ')) + return L + +def _pathNumTrunc(n): + if int(n)==n: return int(n) + return round(n,5) + +def _processGlyph(G, truncate=1, pathReverse=0): + O = [] + P = [] + R = [] + for g in G+(('end',),): + op = g[0] + if O and op in ['moveTo', 'moveToClosed','end']: + if O[0]=='moveToClosed': + O = O[1:] + if pathReverse: + for i in xrange(0,len(P),2): + P[i+1], P[i] = P[i:i+2] + P.reverse() + O.reverse() + O.insert(0,'moveTo') + O.append('closePath') + i = 0 + if truncate: P = map(_pathNumTrunc,P) + for o in O: + j = i + _PATH_OP_ARG_COUNT[_PATH_OP_NAMES.index(o)] + if o=='closePath': + R.append(o) + else: + R.append((o,)+ tuple(P[i:j])) + i = j + O = [] + P = [] + O.append(op) + P.extend(g[1:]) + return R + +def _text2PathDescription(text, x=0, y=0, fontName='Times-Roman', fontSize=1000, + anchor='start', truncate=1, pathReverse=0): + global _gs + if not _gs: + import _renderPM + _gs = _renderPM.gstate(1,1) + from reportlab.graphics import renderPM + renderPM._setFont(_gs,fontName,fontSize) + P = [] + if not anchor =='start': + textLen = stringWidth(text, fontName,fontSize) + if text_anchor=='end': + x = x-textLen + elif text_anchor=='middle': + x = x - textLen/2. + for g in _gs._stringPath(text,x,y): + P.extend(_processGlyph(g,truncate=truncate,pathReverse=pathReverse)) + return P + +def _text2Path(text, x=0, y=0, fontName='Times-Roman', fontSize=1000, + anchor='start', truncate=1, pathReverse=0): + return definePath(_text2PathDescription(text,x=x,y=y,fontName=fontName, + fontSize=fontSize,anchor=anchor,truncate=truncate,pathReverse=pathReverse)) + +_BA2TA={'w':'start','nw':'start','sw':'start','e':'end', 'ne': 'end', 'se':'end', 'n':'middle','s':'middle','c':'middle'} +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), + y = AttrMapValue(isNumber), + dx = AttrMapValue(isNumber), + dy = AttrMapValue(isNumber), + angle = AttrMapValue(isNumber), + boxAnchor = AttrMapValue(isBoxAnchor), + boxStrokeColor = AttrMapValue(isColorOrNone), + boxStrokeWidth = AttrMapValue(isNumber), + boxFillColor = AttrMapValue(isColorOrNone), + boxTarget = AttrMapValue(isString), + fillColor = AttrMapValue(isColorOrNone), + strokeColor = AttrMapValue(isColorOrNone), + strokeWidth = AttrMapValue(isNumber), + text = AttrMapValue(isString), + fontName = AttrMapValue(isString), + fontSize = AttrMapValue(isNumber), + leading = AttrMapValue(isNumberOrNone), + width = AttrMapValue(isNumberOrNone), + maxWidth = AttrMapValue(isNumberOrNone), + height = AttrMapValue(isNumberOrNone), + textAnchor = AttrMapValue(isTextAnchor), + visible = AttrMapValue(isBoolean,desc="True if the label is to be drawn"), + topPadding = AttrMapValue(isNumber,'padding at top of box'), + leftPadding = AttrMapValue(isNumber,'padding at left of box'), + rightPadding = AttrMapValue(isNumber,'padding at right of box'), + bottomPadding = AttrMapValue(isNumber,'padding at bottom of box'), + ) + + 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, + ) + + 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._lines = string.split(self._text, '\n') + self._lineWidths = [] + topPadding = self.topPadding + leftPadding = self.leftPadding + rightPadding = self.rightPadding + bottomPadding = self.bottomPadding + SW = lambda text, fN=self.fontName, fS=self.fontSize: stringWidth(text, fN, fS) + if self.maxWidth: + L = [] + for l in self._lines: + L[-1:-1] = _simpleSplit(l,self.maxWidth,SW) + self._lines = L + if not self.width: + w = 0 + for line in self._lines: + thisWidth = SW(line) + self._lineWidths.append(thisWidth) + w = max(w,thisWidth) + self._width = w+leftPadding+rightPadding + else: + self._width = self.width + self._height = self.height or ((self.leading or 1.2*self.fontSize) * 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 draw(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.fontSize + 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 or 1.2*fontSize) + 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 = y - leading + else: + for line in self._lines: + s = String(x, y, line) + s.textAnchor = textAnchor + s.fontName = fontName + s.fontSize = fontSize + s.fillColor = fillColor + g.add(s) + y = y - leading + + return g + +class LabelDecorator: + _attrMap = AttrMap( + x = AttrMapValue(isNumberOrNone), + y = AttrMapValue(isNumberOrNone), + dx = AttrMapValue(isNumberOrNone), + dy = AttrMapValue(isNumberOrNone), + angle = AttrMapValue(isNumberOrNone), + boxAnchor = AttrMapValue(isBoxAnchor), + boxStrokeColor = AttrMapValue(isColorOrNone), + boxStrokeWidth = AttrMapValue(isNumberOrNone), + boxFillColor = AttrMapValue(isColorOrNone), + fillColor = AttrMapValue(isColorOrNone), + strokeColor = AttrMapValue(isColorOrNone), + strokeWidth = AttrMapValue(isNumberOrNone), + fontName = AttrMapValue(isNoneOrString), + fontSize = AttrMapValue(isNumberOrNone), + leading = AttrMapValue(isNumberOrNone), + width = AttrMapValue(isNumberOrNone), + maxWidth = AttrMapValue(isNumberOrNone), + height = AttrMapValue(isNumberOrNone), + textAnchor = AttrMapValue(isTextAnchor), + visible = AttrMapValue(isBoolean,desc="True if the label is to be drawn"), + ) + + def __init__(self): + self.textAnchor = 'start' + self.boxAnchor = 'w' + for a in self._attrMap.keys(): + if not hasattr(self,a): setattr(self,a,None) + + 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): + flipXY = chart._flipXY + A = chart.categoryAxis + jA = A.joinAxis + if val>=0: + mode = self.posMode + delta = self.pos + else: + mode = self.negMode + delta = self.neg + if flipXY: + v = A._x + else: + v = A._y + if jA: + if flipXY: + _v = jA._x + else: + _v = jA._y + if mode=='high': + v = _v + jA._length + elif mode=='low': + v = _v + elif mode=='bar': + v = _v+val + return v+delta + +NoneOrInstanceOfLabelOffset=NoneOr(isInstanceOf(LabelOffset)) + +class BarChartLabel(Label): + """ + An extended Label allowing for nudging, lines visibility etc + """ + _attrMap = AttrMap( + BASE=Label, + 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"), + ) + + def __init__(self): + Label.__init__(self) + self.lineStrokeWidth = 0 + self.lineStrokeColor = None + self.nudge = 0 + self.fixedStart = self.fixedEnd = None + self._pmv = 0 + + def _getBoxAnchor(self): + a = self.boxAnchor + if self._pmv<0: a = {'nw':'se','n':'s','ne':'sw','w':'e','c':'c','e':'w','sw':'ne','s':'n','se':'nw'}[a] + return a + + def _getTextAnchor(self): + a = self.textAnchor + if self._pmv<0: a = {'start':'end', 'middle':'middle', 'end':'start'}[a] + return a + +class NA_Label(BarChartLabel): + """ + An extended Label allowing for nudging, lines visibility etc + """ + _attrMap = AttrMap( + BASE=BarChartLabel, + text = AttrMapValue(isNoneOrString, desc="Text to be used for N/A values"), + ) + def __init__(self): + BarChartLabel.__init__(self) + self.text = 'n/a' +NoneOrInstanceOfNA_Label=NoneOr(isInstanceOf(NA_Label)) diff --git a/bin/reportlab/graphics/charts/utils.py b/bin/reportlab/graphics/charts/utils.py new file mode 100644 index 00000000000..c654428f5c1 --- /dev/null +++ b/bin/reportlab/graphics/charts/utils.py @@ -0,0 +1,191 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/utils.py +"Utilities used here and there." +__version__=''' $Id: utils.py 2385 2004-06-17 15:26:05Z rgbecker $ ''' + +from time import mktime, gmtime, strftime +import string + + +### Dinu's stuff used in some line plots (likely to vansih). + +def mkTimeTuple(timeString): + "Convert a 'dd/mm/yyyy' formatted string to a tuple for use in the time module." + + list = [0] * 9 + dd, mm, yyyy = map(int, string.split(timeString, '/')) + list[:3] = [yyyy, mm, dd] + + return tuple(list) + + +def str2seconds(timeString): + "Convert a number of seconds since the epoch into a date string." + + return mktime(mkTimeTuple(timeString)) + + +def seconds2str(seconds): + "Convert a date string into the number of seconds since the epoch." + + return strftime('%Y-%m-%d', gmtime(seconds)) + + +### Aaron's rounding function for making nice values on axes. + +from math import log10 + +def nextRoundNumber(x): + """Return the first 'nice round number' greater than or equal to x + + Used in selecting apropriate tick mark intervals; we say we want + an interval which places ticks at least 10 points apart, work out + what that is in chart space, and ask for the nextRoundNumber(). + Tries the series 1,2,5,10,20,50,100.., going up or down as needed. + """ + + #guess to nearest order of magnitude + if x in (0, 1): + return x + + if x < 0: + return -1.0 * nextRoundNumber(-x) + else: + lg = int(log10(x)) + + if lg == 0: + if x < 1: + base = 0.1 + else: + base = 1.0 + elif lg < 0: + base = 10.0 ** (lg - 1) + else: + base = 10.0 ** lg # e.g. base(153) = 100 + # base will always be lower than x + + if base >= x: + return base * 1.0 + elif (base * 2) >= x: + return base * 2.0 + elif (base * 5) >= x: + return base * 5.0 + else: + return base * 10.0 + + +### Robin's stuff from rgb_ticks. + +from math import log10, floor + +_intervals=(.1, .2, .25, .5) +_j_max=len(_intervals)-1 + + +def find_interval(lo,hi,I=5): + 'determine tick parameters for range [lo, hi] using I intervals' + + if lo >= hi: + if lo==hi: + if lo==0: + lo = -.1 + hi = .1 + else: + lo = 0.9*lo + hi = 1.1*hi + else: + raise ValueError, "lo>hi" + x=(hi - lo)/float(I) + b= (x>0 and (x<1 or x>10)) and 10**floor(log10(x)) or 1 + b = b + while 1: + a = x/b + if a<=_intervals[-1]: break + b = b*10 + + j = 0 + while a>_intervals[j]: j = j + 1 + + while 1: + ss = _intervals[j]*b + n = lo/ss + l = int(n)-(n<0) + n = ss*l + x = ss*(l+I) + a = I*ss + if n>0: + if a>=hi: + n = 0.0 + x = a + elif hi<0: + a = -a + if lo>a: + n = a + x = 0 + if hi<=x and n<=lo: break + j = j + 1 + if j>_j_max: + j = 0 + b = b*10 + return n, x, ss, lo - n + x - hi + + +def find_good_grid(lower,upper,n=(4,5,6,7,8,9), grid=None): + if grid: + t = divmod(lower,grid)[0] * grid + hi, z = divmod(upper,grid) + if z>1e-8: hi = hi+1 + hi = hi*grid + else: + try: + n[0] + except TypeError: + n = xrange(max(1,n-2),max(n+3,2)) + + w = 1e308 + for i in n: + z=find_interval(lower,upper,i) + if z[3]<w: + t, hi, grid = z[:3] + w=z[3] + return t, hi, grid + + +def ticks(lower, upper, n=(4,5,6,7,8,9), split=1, percent=0, grid=None): + ''' + return tick positions and labels for range lower<=x<=upper + n=number of intervals to try (can be a list or sequence) + split=1 return ticks then labels else (tick,label) pairs + ''' + t, hi, grid = find_good_grid(lower, upper, n, grid) + power = floor(log10(grid)) + if power==0: power = 1 + w = grid/10.**power + w = int(w)!=w + + if power > 3 or power < -3: + format = '%+'+`w+7`+'.0e' + else: + if power >= 0: + digits = int(power)+w + format = '%' + `digits`+'.0f' + else: + digits = w-int(power) + format = '%'+`digits+2`+'.'+`digits`+'f' + + if percent: format=format+'%%' + T = [] + n = int(float(hi-t)/grid+0.1)+1 + if split: + labels = [] + for i in xrange(n): + v = t+grid*i + T.append(v) + labels.append(format % v) + return T, labels + else: + for i in xrange(n): + v = t+grid*i + T.append((v, format % v)) + return T
\ No newline at end of file diff --git a/bin/reportlab/graphics/charts/utils3d.py b/bin/reportlab/graphics/charts/utils3d.py new file mode 100644 index 00000000000..ca6d0f8d3d5 --- /dev/null +++ b/bin/reportlab/graphics/charts/utils3d.py @@ -0,0 +1,233 @@ +from reportlab.lib import colors +from reportlab.lib.attrmap import * +from reportlab.pdfgen.canvas import Canvas +from reportlab.graphics.shapes import Group, Drawing, Ellipse, Wedge, String, STATE_DEFAULTS, Polygon, Line + +def _getShaded(col,shd=None,shading=0.1): + if shd is None: + from reportlab.lib.colors import Blacker + if col: shd = Blacker(col,1-shading) + return shd + +def _getLit(col,shd=None,lighting=0.1): + if shd is None: + from reportlab.lib.colors import Whiter + if col: shd = Whiter(col,1-lighting) + return shd + + +def _draw_3d_bar(G, x1, x2, y0, yhigh, xdepth, ydepth, + fillColor=None, fillColorShaded=None, + strokeColor=None, strokeWidth=1, shading=0.1): + fillColorShaded = _getShaded(fillColor,None,shading) + fillColorShadedTop = _getShaded(fillColor,None,shading/2.0) + + def _add_3d_bar(x1, x2, y1, y2, xoff, yoff, + G=G,strokeColor=strokeColor, strokeWidth=strokeWidth, fillColor=fillColor): + G.add(Polygon((x1,y1, x1+xoff,y1+yoff, x2+xoff,y2+yoff, x2,y2), + strokeWidth=strokeWidth, strokeColor=strokeColor, fillColor=fillColor,strokeLineJoin=1)) + + usd = max(y0, yhigh) + if xdepth or ydepth: + if y0!=yhigh: #non-zero height + _add_3d_bar( x2, x2, y0, yhigh, xdepth, ydepth, fillColor=fillColorShaded) #side + + _add_3d_bar(x1, x2, usd, usd, xdepth, ydepth, fillColor=fillColorShadedTop) #top + + G.add(Polygon((x1,y0,x2,y0,x2,yhigh,x1,yhigh), + strokeColor=strokeColor, strokeWidth=strokeWidth, fillColor=fillColor,strokeLineJoin=1)) #front + + if xdepth or ydepth: + G.add(Line( x1, usd, x2, usd, strokeWidth=strokeWidth, strokeColor=strokeColor or fillColorShaded)) + +class _YStrip: + def __init__(self,y0,y1, slope, fillColor, fillColorShaded, shading=0.1): + self.y0 = y0 + self.y1 = y1 + self.slope = slope + self.fillColor = fillColor + self.fillColorShaded = _getShaded(fillColor,fillColorShaded,shading) + +def _ystrip_poly( x0, x1, y0, y1, xoff, yoff): + return [x0,y0,x0+xoff,y0+yoff,x1+xoff,y1+yoff,x1,y1] + + +def _make_3d_line_info( G, x0, x1, y0, y1, z0, z1, + theta_x, theta_y, + fillColor, fillColorShaded=None, tileWidth=1, + strokeColor=None, strokeWidth=None, strokeDashArray=None, + shading=0.1): + zwidth = abs(z1-z0) + xdepth = zwidth*theta_x + ydepth = zwidth*theta_y + depth_slope = xdepth==0 and 1e150 or -ydepth/float(xdepth) + + x = float(x1-x0) + slope = x==0 and 1e150 or (y1-y0)/x + + c = slope>depth_slope and _getShaded(fillColor,fillColorShaded,shading) or fillColor + zy0 = z0*theta_y + zx0 = z0*theta_x + + tileStrokeWidth = 0.6 + if tileWidth is None: + D = [(x1,y1)] + else: + T = ((y1-y0)**2+(x1-x0)**2)**0.5 + tileStrokeWidth *= tileWidth + if T<tileWidth: + D = [(x1,y1)] + else: + n = int(T/float(tileWidth))+1 + dx = float(x1-x0)/n + dy = float(y1-y0)/n + D = [] + a = D.append + for i in xrange(1,n): + a((x0+dx*i,y0+dy*i)) + + a = G.add + x_0 = x0+zx0 + y_0 = y0+zy0 + for x,y in D: + x_1 = x+zx0 + y_1 = y+zy0 + P = Polygon(_ystrip_poly(x_0, x_1, y_0, y_1, xdepth, ydepth), + fillColor = c, strokeColor=c, strokeWidth=tileStrokeWidth) + a((0,z0,z1,x_0,y_0,P)) + x_0 = x_1 + y_0 = y_1 + +from math import pi, sin, cos +_pi_2 = pi*0.5 +_2pi = 2*pi +_180_pi=180./pi + +def _2rad(angle): + return angle/_180_pi + +def mod_2pi(radians): + radians = radians % _2pi + if radians<-1e-6: radians += _2pi + return radians + +def _2deg(o): + return o*_180_pi + +def _360(a): + a %= 360 + if a<-1e-6: a += 360 + return a + +_ZERO = 1e-8 +_ONE = 1-_ZERO +class _Segment: + def __init__(self,s,i,data): + S = data[s] + x0 = S[i-1][0] + y0 = S[i-1][1] + x1 = S[i][0] + y1 = S[i][1] + if x1<x0: + x0,y0,x1,y1 = x1,y1,x0,y0 + # (y-y0)*(x1-x0) = (y1-y0)*(x-x0) + # (x1-x0)*y + (y0-y1)*x = y0*(x1-x0)+x0*(y0-y1) + # a*y+b*x = c + self.a = float(x1-x0) + self.b = float(y1-y0) + self.x0 = x0 + self.x1 = x1 + self.y0 = y0 + self.y1 = y1 + self.series = s + self.i = i + self.s = s + + def __str__(self): + return '[(%s,%s),(%s,%s)]' % (self.x0,self.y0,self.x1,self.y1) + + __repr__ = __str__ + + def intersect(self,o,I): + '''try to find an intersection with _Segment o + ''' + x0 = self.x0 + ox0 = o.x0 + assert x0<=ox0 + if ox0>self.x1: return 1 + if o.s==self.s and o.i in (self.i-1,self.i+1): return + a = self.a + b = self.b + oa = o.a + ob = o.b + det = ob*a - oa*b + if -1e-8<det<1e-8: return + dx = x0 - ox0 + dy = self.y0 - o.y0 + u = (oa*dy - ob*dx)/det + ou = (a*dy - b*dx)/det + if u<0 or u>1 or ou<0 or ou>1: return + x = x0 + u*a + y = self.y0 + u*b + if _ZERO<u<_ONE: + t = self.s,self.i,x,y + if t not in I: I.append(t) + if _ZERO<ou<_ONE: + t = o.s,o.i,x,y + if t not in I: I.append(t) + +def _segCmp(a,b): + return cmp((a.x0,a.x1,a.y0,a.y1,a.s,a.i),(b.x0,b.x1,b.y0,b.y1,b.s,b.i)) + +def find_intersections(data,small=0): + ''' + data is a sequence of series + each series is a list of (x,y) coordinates + where x & y are ints or floats + + find_intersections returns a sequence of 4-tuples + i, j, x, y + + where i is a data index j is an insertion position for data[i] + and x, y are coordinates of an intersection of series data[i] + with some other series. If correctly implemented we get all such + intersections. We don't count endpoint intersections and consider + parallel lines as non intersecting (even when coincident). + We ignore segments that have an estimated size less than small. + ''' + + #find all line segments + S = [] + a = S.append + for s in xrange(len(data)): + ds = data[s] + if not ds: continue + n = len(ds) + if n==1: continue + for i in xrange(1,n): + seg = _Segment(s,i,data) + if seg.a+abs(seg.b)>=small: a(seg) + S.sort(_segCmp) + I = [] + n = len(S) + for i in xrange(0,n-1): + s = S[i] + for j in xrange(i+1,n): + if s.intersect(S[j],I)==1: break + I.sort() + return I + +if __name__=='__main__': + from reportlab.graphics.shapes import Drawing + from reportlab.lib.colors import lightgrey, pink + D = Drawing(300,200) + _draw_3d_bar(D, 10, 20, 10, 50, 5, 5, fillColor=lightgrey, strokeColor=pink) + _draw_3d_bar(D, 30, 40, 10, 45, 5, 5, fillColor=lightgrey, strokeColor=pink) + + D.save(formats=['pdf'],outDir='.',fnRoot='_draw_3d_bar') + + print find_intersections([[(0,0.5),(1,0.5),(0.5,0),(0.5,1)],[(.2666666667,0.4),(0.1,0.4),(0.1,0.2),(0,0),(1,1)],[(0,1),(0.4,0.1),(1,0.1)]]) + print find_intersections([[(0.1, 0.2), (0.1, 0.4)], [(0, 1), (0.4, 0.1)]]) + print find_intersections([[(0.2, 0.4), (0.1, 0.4)], [(0.1, 0.8), (0.4, 0.1)]]) + print find_intersections([[(0,0),(1,1)],[(0.4,0.1),(1,0.1)]]) + print find_intersections([[(0,0.5),(1,0.5),(0.5,0),(0.5,1)],[(0,0),(1,1)],[(0.1,0.8),(0.4,0.1),(1,0.1)]]) |
