diff options
| author | pinky <> | 2006-12-07 13:41:40 +0000 |
|---|---|---|
| committer | pinky <> | 2006-12-07 13:41:40 +0000 |
| commit | 004a0b996ff8f269451e07346f71a129a1f3fbaf (patch) | |
| tree | 48f43c1fc2ca4c1f4d5648e7e73553d3f4ccae06 /bin/reportlab/graphics | |
New trunk
bzr revid: pinky-3f10ee12cea3c4c75cef44ab04ad33ef47432907
Diffstat (limited to 'bin/reportlab/graphics')
49 files changed, 18675 insertions, 0 deletions
diff --git a/bin/reportlab/graphics/__init__.py b/bin/reportlab/graphics/__init__.py new file mode 100644 index 00000000000..6826e81ffee --- /dev/null +++ b/bin/reportlab/graphics/__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/__init__.py +__version__=''' $Id$ '''
\ No newline at end of file diff --git a/bin/reportlab/graphics/charts/__init__.py b/bin/reportlab/graphics/charts/__init__.py new file mode 100644 index 00000000000..abe27780589 --- /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$ '''
\ 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..e3353660c27 --- /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$ ''' +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..216b420b418 --- /dev/null +++ b/bin/reportlab/graphics/charts/axes.py @@ -0,0 +1,1958 @@ +#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$ ''' + +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) + +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"), + ) + + 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 + + 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): + self._tickValues = range(self._catCount+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 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() + + + 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) + + 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 + + if not (self.categoryNames is None): + catCount = self._catCount + assert len(self.categoryNames) == catCount, \ + "expected %d category names but found %d in axis. \n categories = %s" % ( + len(self.categoryNames), catCount, repr(self.categoryNames) + ) + reverseDirection = self.reverseDirection + barWidth = self._barWidth + _y = self._labelAxisPos() + _x = self._x + + for i in range(catCount): + x = _x + (i+0.5) * barWidth + label = self.labels[i] + label.setOrigin(x, _y) + if reverseDirection: i = catCount-i-1 + label.setText(self.categoryNames[i]) + 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 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() + + if not self.visibleAxis: + return g + + 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) + + 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 + + if not (self.categoryNames is None): + catCount = self._catCount + assert len(self.categoryNames) == catCount, \ + "expected %d category names but found %d in axis" % ( + len(self.categoryNames), catCount + ) + reverseDirection = self.reverseDirection + barWidth = self._barWidth + labels = self.labels + _x = self._labelAxisPos() + _y = self._y + for i in range(catCount): + y = _y + (i+0.5) * barWidth + label = labels[i] + label.setOrigin(_x, y) + if reverseDirection: i = catCount-i-1 + label.setText(self.categoryNames[i]) + 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 + while go: + go = 0 + if do_abf: + valueStep, T, fuzz = self._getValueStepAndTicks(valueMin, valueMax, cache) + fuzz = 1e-8*valueStep + i0 = valueStep*abf[0] + i1 = valueStep*abf[1] + if rrn: v = T[0] + else: v = valueMin + d = v - (cMin-i0) + if abs(v)>fuzz and fuzz < d: + valueMin = valueMin - (d+fuzz) + go = 1 + if rrn: v = T[-1] + else: v = valueMax + d = i1 + cMax - v + if abs(v)>fuzz and fuzz < d: + valueMax = valueMax + (d+fuzz) + go = 1 + + if do_rr: + valueStep, T, fuzz = self._getValueStepAndTicks(valueMin, valueMax, cache) + if rangeRound in ['both','floor'] and valueMin<T[0]-fuzz: + valueMin = T[0]-valueStep + go = 1 + if rangeRound in ['both','ceiling'] and valueMax>T[-1]+fuzz: + valueMax = T[-1]+valueStep + go = 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() + P = [] + valueMin, valueMax, valueStep = self._valueMin, self._valueMax, self._valueStep + fuzz = 1e-8*valueStep + t = int((valueMin-fuzz)/valueStep) * valueStep + if t >= valueMin-fuzz: P.append(t) + t = t + valueStep + while t <= valueMax+fuzz: + P.append(t) + t = t + valueStep + return P + + 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): + for tick in self._tickValues: + try: + if int(tick)!=tick: return 0 + except: + return 0 + return 1 + + 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 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() + + if not self.visibleAxis: + return g + + 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) + + 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 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() + + if not self.visibleAxis: + return g + + 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) + + 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..5c57184060c --- /dev/null +++ b/bin/reportlab/graphics/charts/barcharts.py @@ -0,0 +1,1943 @@ +#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$ ''' + +import string, copy +from types import FunctionType, StringType + +from reportlab.lib import colors +from reportlab.lib.validators import isNumber, isColor, isColorOrNone, isListOfStrings, SequenceOf, isBoolean, isNoneOrShape +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 +from reportlab.graphics.charts.axes import 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.'), + ) + + 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 __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' + if self._flipXY: + return x + width + (width>=0 and 1 or -1) * label.nudge, y + 0.5*height + else: + return x + 0.5*width, y + height + (height>=0 and 1 or -1) * label.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..e8829b29996 --- /dev/null +++ b/bin/reportlab/graphics/charts/doughnut.py @@ -0,0 +1,372 @@ +#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$ ''' + +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.areas import PlotArea +from reportlab.graphics.charts.textlabels import Label +from reportlab.graphics.widgets.markers import Marker + +class SectorProperties(PropHolder): + """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( + 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), + ) + + 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 + + +class Doughnut(Widget): + _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"), + ) + + 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.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)) + else: + normData = self.normalizeData(self.data) + n = len(normData) + + #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 errors 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() + i = 0 + sn = 0 + + startAngle = self.startAngle #% 360 + if type(self.data[0]) in (ListType, TupleType): + #multi-series doughnut + styleCount = len(self.slices) + iradius = (self.height/5.0)/len(self.data) + for series in normData: + 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 + + 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) + i = i + 1 + sn = sn + 1 + + else: + #single series doughnut + styleCount = len(self.slices) + 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..f991bd7ed7c --- /dev/null +++ b/bin/reportlab/graphics/charts/legends.py @@ -0,0 +1,486 @@ +#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$ ''' + +import string, copy + +from reportlab.lib import colors +from reportlab.lib.validators import isNumber, OneOf, isString, isColorOrNone, isNumberOrNone, isListOfNumbersOrNone +from reportlab.lib.attrmap import * +from reportlab.pdfbase.pdfmetrics import stringWidth, getFont +from reportlab.graphics.widgetbase import Widget +from reportlab.graphics.shapes import Drawing, Group, String, Rect, Line, STATE_DEFAULTS + + +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"), + 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"), + callout = AttrMapValue(None, desc="a user callout(self,g,x,y,(color,text))"), + ) + + 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'] + + def _calculateMaxWidth(self, colorNamePairs): + "Calculate the maximum width of some given strings." + m = 0 + for t in map(lambda p:str(p[1]),colorNamePairs): + if t: + for s in string.split(t,'\n'): + m = max(m,stringWidth(s, self.fontName, self.fontSize)) + return m + + + def _calcHeight(self): + deltay = self.deltay + dy = self.dy + thisy = upperlefty = self.y - dy + ascent=getFont(self.fontName).face.ascent/1000. + if ascent==0: ascent=0.718 # default (from helvetica) + leading = self.fontSize*1.2 + columnCount = 0 + count = 0 + lowy = upperlefty + for unused, name in colorNamePairs: + T = string.split(name and str(name) or '','\n') + S = [] + # thisy+dy/2 = y+leading/2 + y = thisy+(dy-ascent)*0.5-leading + newy = thisy-max(deltay,len(S)*leading) + lowy = min(y,newy) + if count == columnMaximum-1: + count = 0 + thisy = upperlefty + columnCount = columnCount + 1 + else: + thisy = newy + count = count+1 + return upperlefty - lowy + + def draw(self): + g = Group() + colorNamePairs = self.colorNamePairs + thisx = upperleftx = self.x + thisy = upperlefty = self.y - self.dy + dx, dy, alignment, columnMaximum = self.dx, self.dy, self.alignment, self.columnMaximum + deltax, deltay, dxTextSpace = self.deltax, self.deltay, self.dxTextSpace + fontName, fontSize, fillColor = self.fontName, self.fontSize, self.fillColor + strokeWidth, strokeColor = self.strokeWidth, self.strokeColor + leading = fontSize*1.2 + if not deltay: + deltay = max(dy,leading)+self.autoYPadding + if not deltax: + maxWidth = self._calculateMaxWidth(colorNamePairs) + deltax = maxWidth+dx+dxTextSpace+self.autoXPadding + else: + if alignment=='left': maxWidth = self._calculateMaxWidth(colorNamePairs) + + 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=ascent*fontSize # normalize + + columnCount = 0 + count = 0 + callout = getattr(self,'callout',None) + for col, name in colorNamePairs: + T = string.split(name and str(name) or '','\n') + S = [] + # thisy+dy/2 = y+leading/2 + y = thisy+(dy-ascent)*0.5 + if callout: callout(self,g,thisx,y,colorNamePairs[count]) + if alignment == "left": + for t in T: + # align text to left + s = String(thisx+maxWidth,y,t) + s.textAnchor = "end" + S.append(s) + y = y-leading + x = thisx+maxWidth+dxTextSpace + elif alignment == "right": + for t in T: + # align text to right + s = String(thisx+dx+dxTextSpace, y, t) + s.textAnchor = "start" + S.append(s) + y = y-leading + x = thisx + else: + raise ValueError, "bad alignment" + + # Make a 'normal' color swatch... + if isinstance(col, colors.Color): + r = Rect(x, thisy, dx, dy) + r.fillColor = col + r.strokeColor = strokeColor + r.strokeWidth = strokeWidth + g.add(r) + else: + #try and see if we should do better. + 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 count%columnMaximum == columnMaximum-1: + thisx = thisx+deltax + thisy = upperlefty + columnCount = columnCount + 1 + else: + thisy = thisy-max(deltay,len(S)*leading) + count = count+1 + + return g + + +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. + """ + + _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 line-swatches"), + deltay = AttrMapValue(isNumberOrNone, desc="y-distance between neighbouring line-swatches"), + dxTextSpace = AttrMapValue(isNumber, desc="Distance between line-swatches and text"), + autoXPadding = AttrMapValue(isNumber, desc="x Padding between columns if deltax=None"), + autoYPadding = AttrMapValue(isNumber, desc="y Padding between rows if deltay=None"), + dx = AttrMapValue(isNumber, desc="Width of line-swatch - ie length of the line"), + dy = AttrMapValue(isNumber, desc="Height of line-swatch - ie strokeWidth to be used for the line"), + columnMaximum = AttrMapValue(isNumber, desc="Max. number of items per column"), + alignment = AttrMapValue(OneOf("left", "right"), desc="Alignment of text with respect to line-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="Stroke color of the line-swatches"), + strokeWidth = AttrMapValue(isNumber, desc="Width of the line-swatches"), + callout = AttrMapValue(None, desc="a user callout(self,g,x,y,(color,text))"), + ) + + def __init__(self): + Legend.__init__(self) + + # Size of swatch rectangle. + self.dx = 10 #width of line + self.dy = 2 #strokeWidth for line + + # Color/name pairs. + self.colorNamePairs = [] + for col, colName in [ (colors.red, "red"), + (colors.blue, "blue"), + (colors.green, "green"), + (colors.pink, "pink"), + (colors.yellow, "yellow") ]: + l = LineSwatch() + l.strokeColor = col + self.colorNamePairs.append((l, colName)) + + # Font name and size of the labels. + self.fillColor = STATE_DEFAULTS['fillColor'] + self.strokeColor = STATE_DEFAULTS['strokeColor'] + self.strokeWidth = STATE_DEFAULTS['strokeWidth'] + + def draw(self): + g = Group() + colorNamePairs = self.colorNamePairs + thisx = upperleftx = self.x + thisy = upperlefty = self.y - self.dy + dx, dy, alignment, columnMaximum = self.dx, self.dy, self.alignment, self.columnMaximum + deltax, deltay, dxTextSpace = self.deltax, self.deltay, self.dxTextSpace + fontName, fontSize, fillColor = self.fontName, self.fontSize, self.fillColor + strokeWidth, strokeColor = self.strokeWidth, self.strokeColor + leading = fontSize*1.2 + if not deltay: + deltay = max(dy,leading)+self.autoYPadding + if not deltax: + maxWidth = self._calculateMaxWidth(colorNamePairs) + deltax = maxWidth+dx+dxTextSpace+self.autoXPadding + else: + if alignment=='left': maxWidth = self._calculateMaxWidth(colorNamePairs) + + 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=ascent*fontSize # normalize + + columnCount = 0 + count = 0 + callout = getattr(self,'callout',None) + for col, name in colorNamePairs: + T = string.split(name and str(name) or '','\n') + S = [] + # thisy+dy/2 = y+leading/2 + y = thisy+(dy-ascent)*0.5 + if callout: callout(self,g,thisx,y,colorNamePairs[count]) + if alignment == "left": + for t in T: + # align text to left + s = String(thisx+maxWidth,y,t) + s.textAnchor = "end" + S.append(s) + y = y-leading + x = thisx+maxWidth+dxTextSpace + elif alignment == "right": + for t in T: + # align text to right + s = String(thisx+dx+dxTextSpace, y, t) + s.textAnchor = "start" + S.append(s) + y = y-leading + x = thisx + else: + raise ValueError, "bad alignment" + + # Make a 'normal' color line-swatch... + if isinstance(col, colors.Color): + l = LineSwatch() + l.x = x + l.y = thisy + l.width = dx + l.height = dy + l.strokeColor = col + g.add(l) + else: + #try and see if we should do better. + 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 count%columnMaximum == columnMaximum-1: + thisx = thisx+deltax + thisy = upperlefty + columnCount = columnCount + 1 + else: + thisy = thisy-max(deltay,len(S)*leading) + count = count+1 + + 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 = string.split('red green blue yellow pink black white', ' ') + items = map(lambda i:(getattr(colors, i), i), items) + legend.colorNamePairs = items + + d.add(legend, 'legend') + + return d + + +def sample1c(): + "Make sample legend." + + d = Drawing(200, 100) + + legend = Legend() + legend.alignment = 'right' + legend.x = 0 + legend.y = 100 + legend.dxTextSpace = 5 + items = string.split('red green blue yellow pink black white', ' ') + 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 = string.split('red green blue yellow pink black white', ' ') + 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 = string.split('red green blue yellow pink black white', ' ') + 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 = string.split('red green blue yellow pink black white', ' ') + 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..c98c05c05ed --- /dev/null +++ b/bin/reportlab/graphics/charts/linecharts.py @@ -0,0 +1,656 @@ +#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$ ''' + +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 +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.'), + ) + +class LineChart(PlotArea): + 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..7ada465a380 --- /dev/null +++ b/bin/reportlab/graphics/charts/lineplots.py @@ -0,0 +1,1096 @@ +#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$ ''' + +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.'), + ) + +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 Filler: + '''mixin providing simple polygon fill''' + def fill(self, lp, g, rowNo, rowColor, points): + self.points[:] = points + g.add(self) + +class ShadedPolyFiller(Filler,ShadedPolygon): + pass + +class PolyFiller(Filler,Polygon): + pass + +class LinePlot(PlotArea): + """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: + points = points + [inFillX1,inFillY,inFillX0,inFillY] + filler = getattr(rowStyle, 'filler', None) + if filler: + filler.fill(self,inFillG,rowNo,rowColor,points) + else: + inFillG.add(Polygon(points,fillColor=rowColor,strokeColor=rowColor,strokeWidth=0.1)) + else: + 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 makeSwatchSample(self,rowNo, x, y, width, height): + styleCount = len(self.lines) + styleIdx = rowNo % styleCount + rowColor = self.lines[styleIdx].strokeColor + + if self.joinedLines: + dash = getattr(self.lines[styleIdx], 'strokeDashArray', getattr(self.lines,'strokeDashArray',None)) + strokeWidth= getattr(self.lines[styleIdx], 'strokeWidth', getattr(self.lines[styleIdx], 'strokeWidth',None)) + L = Line(x,y,x+width,y+height,strokeColor=rowColor,strokeLineCap=0) + if strokeWidth: L.strokeWidth = strokeWidth + if dash: L.strokeDashArray = dash + else: + L = None + + if hasattr(self.lines[styleIdx], 'symbol'): + S = self.lines[styleIdx].symbol + elif hasattr(self.lines, 'symbol'): + S = self.lines.symbol + else: + S = None + + if S: S = uSymbol2Symbol(S,x+width/2.,y+height/2.,rowColor) + if S and L: + g = Group() + g.add(S) + g.add(L) + return g + return S or L + + 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: + 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..6f0168c4cf3 --- /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$ ''' +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..afa688ce45e --- /dev/null +++ b/bin/reportlab/graphics/charts/piecharts.py @@ -0,0 +1,863 @@ +#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$ ''' + +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 +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 +from reportlab.graphics.widgetbase import Widget, TypedPropertyCollection, PropHolder +from textlabels import Label + +_ANGLE2BOXANCHOR={0:'w', 45:'sw', 90:'s', 135:'se', 180:'e', 225:'ne', 270:'n', 315: 'nw', -45: 'nw'} +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 = _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'), + ) + + 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 + +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" + 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 Pie(Widget): + _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 pie bounding box. Need not be same as width.'), + height = AttrMapValue(isNumber, desc='height of pie bounding box. Need not be same as height.'), + 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 thresh holding, not used yet.'), + ) + other_threshold=None + + def __init__(self): + self.x = 0 + self.y = 0 + self.width = 100 + self.height = 100 + self.data = [1] + self.labels = None # or list of strings + self.startAngle = 90 + self.direction = "clockwise" + self.simpleLabels = 1 + + 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 + + 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 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 makeWedges(self): + # normalize slice data + normData = self.normalizeData() + n = len(normData) + labels = _fixLabels(self.labels,n) + + 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() + i = 0 + styleCount = len(self.slices) + + startAngle = self.startAngle #% 360 + 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 wedges + #all having the default style + wedgeStyle = self.slices[i%styleCount] + + # is it a popout? + cx, cy = centerx, centery + if wedgeStyle.popout <> 0: + # pop out the wedge + averageAngle = (a1+a2)/2.0 + aveAngleRadians = averageAngle * pi/180.0 + popdistance = wedgeStyle.popout + cx = centerx + popdistance * cos(aveAngleRadians) + cy = centery + popdistance * sin(aveAngleRadians) + + 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) + text = labels[i] + if text: + averageAngle = (a1+a2)/2.0 + aveAngleRadians = averageAngle*pi/180.0 + labelRadius = wedgeStyle.labelRadius + labelX = cx + (0.5 * self.width * cos(aveAngleRadians) * labelRadius) + labelY = cy + (0.5 * self.height * sin(aveAngleRadians) * labelRadius) + _addWedgeLabel(self,text,g.add,averageAngle,labelX,labelY,wedgeStyle) + + startAngle = endAngle + i = i + 1 + + return g + + def draw(self): + g = Group() + g.add(self.makeWedges()) + return g + +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 +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 + + 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 + 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)) + #print '%d: %.2f %.2f --> %s' %(len(_sl3d)-1,angle0,angle1,_sl3d[-1]) + + labels = _fixLabels(self.labels,n) + a0 = _3d_angle + a1 = _3d_angle+180 + T = [] + S = [] + L = [] + + class WedgeLabel3d(WedgeLabel): + def _checkDXY(self,ba): + if ba[0]=='n': + if not hasattr(self,'_ody'): + self._ody = self.dy + self.dy = -self._ody + self._ydepth_3d + WedgeLabel3d._ydepth_3d = self._ydepth_3d + + 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 + _addWedgeLabel(self,text,L.append,mid,OX(i,mid,0),OY(i,mid,0),style,labelClass=WedgeLabel3d) + self._radiusx = radiusx + self._radiusy = radiusy + + S.sort(lambda a,b: -cmp(a[0],b[0])) + 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..0e88ff1ff7f --- /dev/null +++ b/bin/reportlab/graphics/charts/spider.py @@ -0,0 +1,353 @@ + #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$ ''' + +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 +from reportlab.lib.attrmap import * +from reportlab.pdfgen.canvas import Canvas +from reportlab.graphics.shapes import Group, Drawing, Line, Rect, Polygon, 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 + +class StrandProperties(PropHolder): + """This holds descriptive information about concentric 'strands'. + + Line style, whether filled etc. + """ + + _attrMap = AttrMap( + strokeWidth = AttrMapValue(isNumber), + fillColor = AttrMapValue(isColorOrNone), + strokeColor = AttrMapValue(isColorOrNone), + strokeDashArray = AttrMapValue(isListOfNumbersOrNone), + fontName = AttrMapValue(isString), + fontSize = AttrMapValue(isNumber), + fontColor = AttrMapValue(isColorOrNone), + labelRadius = AttrMapValue(isNumber), + markers = AttrMapValue(isBoolean), + markerType = AttrMapValue(isAnything), + markerSize = 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'), + ) + + def __init__(self): + self.strokeWidth = 0 + self.fillColor = None + self.strokeColor = STATE_DEFAULTS["strokeColor"] + self.strokeDashArray = STATE_DEFAULTS["strokeDashArray"] + self.fontName = STATE_DEFAULTS["fontName"] + self.fontSize = STATE_DEFAULTS["fontSize"] + self.fontColor = STATE_DEFAULTS["fillColor"] + self.labelRadius = 1.2 + self.markers = 0 + self.markerType = None + self.markerSize = 0 + 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 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"), + ) + + 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.startAngle = 90 + self.direction = "clockwise" + + self.strands = TypedPropertyCollection(StrandProperties) + self.strands[0].fillColor = colors.cornsilk + self.strands[1].fillColor = colors.cyan + + + def demo(self): + d = Drawing(200, 100) + + sp = SpiderChart() + sp.x = 50 + sp.y = 10 + sp.width = 100 + sp.height = 80 + sp.data = [[10,12,14,16,18,20],[6,8,4,6,8,10]] + sp.labels = ['a','b','c','d','e','f'] + + d.add(sp) + 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 + theMax = 0.0 + for row in data: + for element in row: + assert element >=0, "Cannot do spider plots of negative numbers!" + if element > theMax: + theMax = element + theMax = theMax * (1.0+outer) + + scaled = [] + for row in data: + scaledRow = [] + for element in row: + scaledRow.append(element / theMax) + scaled.append(scaledRow) + return scaled + + + 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) + centerx = self.x + xradius + centery = self.y + yradius + + data = self.normalizeData() + + 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 + + spokes = [] + csa = [] + angle = self.startAngle*pi/180 + direction = self.direction == "clockwise" and -1 or 1 + angleBetween = direction*(2 * pi)/n + markers = self.strands.markers + for i in xrange(n): + car = cos(angle)*radius + sar = sin(angle)*radius + csa.append((car,sar,angle)) + spoke = Line(centerx, centery, centerx + car, centery + sar, strokeWidth = 0.5) + #print 'added spoke (%0.2f, %0.2f) -> (%0.2f, %0.2f)' % (spoke.x1, spoke.y1, spoke.x2, spoke.y2) + spokes.append(spoke) + if labels: + si = self.strands[i] + text = si.label_text + if text is None: text = labels[i] + if text: + labelRadius = si.labelRadius + L = WedgeLabel() + L.x = centerx + labelRadius*car + L.y = centery + labelRadius*sar + L.boxAnchor = si.label_boxAnchor + L._pmv = angle*180/pi + L.dx = si.label_dx + L.dy = si.label_dy + L.angle = si.label_angle + L.boxAnchor = si.label_boxAnchor + L.boxStrokeColor = si.label_boxStrokeColor + L.boxStrokeWidth = si.label_boxStrokeWidth + L.boxFillColor = si.label_boxFillColor + L.strokeColor = si.label_strokeColor + L.strokeWidth = si.label_strokeWidth + L._text = text + L.leading = si.label_leading + L.width = si.label_width + L.maxWidth = si.label_maxWidth + L.height = si.label_height + L.textAnchor = si.label_textAnchor + L.visible = si.label_visible + L.topPadding = si.label_topPadding + L.leftPadding = si.label_leftPadding + L.rightPadding = si.label_rightPadding + L.bottomPadding = si.label_bottomPadding + L.fontName = si.fontName + L.fontSize = si.fontSize + L.fillColor = si.fontColor + spokes.append(L) + angle = angle + angleBetween + + # now plot the polygons + + rowIdx = 0 + for row in data: + # series plot + points = [] + car, sar = csa[-1][:2] + r = row[-1] + points.append(centerx+car*r) + points.append(centery+sar*r) + for i in xrange(n): + car, sar = csa[i][:2] + r = row[i] + points.append(centerx+car*r) + points.append(centery+sar*r) + + # make up the 'strand' + strand = Polygon(points) + strand.fillColor = self.strands[rowIdx].fillColor + strand.strokeColor = self.strands[rowIdx].strokeColor + strand.strokeWidth = self.strands[rowIdx].strokeWidth + strand.strokeDashArray = self.strands[rowIdx].strokeDashArray + + g.add(strand) + + # put in a marker, if it needs one + if markers: + if hasattr(self.strands[rowIdx], 'markerType'): + uSymbol = self.strands[rowIdx].markerType + elif hasattr(self.strands, 'markerType'): + uSymbol = self.strands.markerType + else: + uSymbol = None + m_x = centerx+car*r + m_y = centery+sar*r + m_size = self.strands[rowIdx].markerSize + m_fillColor = self.strands[rowIdx].fillColor + m_strokeColor = self.strands[rowIdx].strokeColor + m_strokeWidth = self.strands[rowIdx].strokeWidth + m_angle = 0 + if type(uSymbol) is type(''): + symbol = makeMarker(uSymbol, + size = m_size, + x = m_x, + y = m_y, + fillColor = m_fillColor, + strokeColor = m_strokeColor, + strokeWidth = m_strokeWidth, + angle = m_angle, + ) + else: + symbol = uSymbol2Symbol(uSymbol,m_x,m_y,m_fillColor) + for k,v in (('size', m_size), ('fillColor', m_fillColor), + ('x', m_x), ('y', m_y), + ('strokeColor',m_strokeColor), ('strokeWidth',m_strokeWidth), + ('angle',m_angle),): + try: + setattr(uSymbol,k,v) + except: + pass + g.add(symbol) + + rowIdx = rowIdx + 1 + + # spokes go over strands + for spoke in spokes: + g.add(spoke) + return g + +def sample1(): + "Make a simple spider chart" + + d = Drawing(400, 400) + + pc = SpiderChart() + pc.x = 50 + pc.y = 50 + pc.width = 300 + pc.height = 300 + pc.data = [[10,12,14,16,14,12], [6,8,10,12,9,15],[7,8,17,4,12,8,3]] + pc.labels = ['a','b','c','d','e','f'] + pc.strands[2].fillColor=colors.palegreen + + d.add(pc) + + return d + + +def sample2(): + "Make a spider chart with markers, but no fill" + + d = Drawing(400, 400) + + pc = SpiderChart() + pc.x = 50 + pc.y = 50 + pc.width = 300 + pc.height = 300 + pc.data = [[10,12,14,16,14,12], [6,8,10,12,9,15],[7,8,17,4,12,8,3]] + pc.labels = ['U','V','W','X','Y','Z'] + pc.strands.strokeWidth = 2 + pc.strands[0].fillColor = None + pc.strands[1].fillColor = None + pc.strands[2].fillColor = None + pc.strands[0].strokeColor = colors.red + pc.strands[1].strokeColor = colors.blue + pc.strands[2].strokeColor = colors.green + pc.strands.markers = 1 + pc.strands.markerType = "FilledDiamond" + pc.strands.markerSize = 6 + + d.add(pc) + + return d + + +if __name__=='__main__': + d = sample1() + from reportlab.graphics.renderPDF import drawToFile + drawToFile(d, 'spider.pdf') + d = sample2() + drawToFile(d, 'spider2.pdf') + #print 'saved spider.pdf' diff --git a/bin/reportlab/graphics/charts/textlabels.py b/bin/reportlab/graphics/charts/textlabels.py new file mode 100644 index 00000000000..9a89258849a --- /dev/null +++ b/bin/reportlab/graphics/charts/textlabels.py @@ -0,0 +1,440 @@ +#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$ ''' +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), + 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, + 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..028f0f70284 --- /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$ ''' + +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..e1ff5403c12 --- /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*pi)/180 + +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)]]) diff --git a/bin/reportlab/graphics/renderPDF.py b/bin/reportlab/graphics/renderPDF.py new file mode 100644 index 00000000000..b3b4c8ab62d --- /dev/null +++ b/bin/reportlab/graphics/renderPDF.py @@ -0,0 +1,361 @@ +#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/renderPDF.py +# renderPDF - draws Drawings onto a canvas +"""Usage: + import renderpdf + renderpdf.draw(drawing, canvas, x, y) +Execute the script to see some test drawings. +changed +""" +__version__=''' $Id$ ''' + +from reportlab.graphics.shapes import * +from reportlab.pdfgen.canvas import Canvas +from reportlab.pdfbase.pdfmetrics import stringWidth +from reportlab.lib.utils import getStringIO +from reportlab import rl_config + +# the main entry point for users... +def draw(drawing, canvas, x, y, showBoundary=rl_config._unset_): + """As it says""" + R = _PDFRenderer() + R.draw(drawing, canvas, x, y, showBoundary=showBoundary) + +from renderbase import Renderer, StateTracker, getStateDelta + +class _PDFRenderer(Renderer): + """This draws onto a PDF document. It needs to be a class + rather than a function, as some PDF-specific state tracking is + needed outside of the state info in the SVG model.""" + + def __init__(self): + self._stroke = 0 + self._fill = 0 + self._tracker = StateTracker() + + def drawNode(self, node): + """This is the recursive method called for each node + in the tree""" + #print "pdf:drawNode", self + #if node.__class__ is Wedge: stop + if not (isinstance(node, Path) and node.isClipPath): + self._canvas.saveState() + + #apply state changes + deltas = getStateDelta(node) + self._tracker.push(deltas) + self.applyStateChanges(deltas, {}) + + #draw the object, or recurse + self.drawNodeDispatcher(node) + + self._tracker.pop() + if not (isinstance(node, Path) and node.isClipPath): + self._canvas.restoreState() + + def drawRect(self, rect): + if rect.rx == rect.ry == 0: + #plain old rectangle + self._canvas.rect( + rect.x, rect.y, + rect.width, rect.height, + stroke=self._stroke, + fill=self._fill + ) + else: + #cheat and assume ry = rx; better to generalize + #pdfgen roundRect function. TODO + self._canvas.roundRect( + rect.x, rect.y, + rect.width, rect.height, rect.rx, + fill=self._fill, + stroke=self._stroke + ) + + def drawImage(self, image): + # currently not implemented in other renderers + if image.path and os.path.exists(image.path): + self._canvas.drawInlineImage( + image.path, + image.x, image.y, + image.width, image.height + ) + + def drawLine(self, line): + if self._stroke: + self._canvas.line(line.x1, line.y1, line.x2, line.y2) + + def drawCircle(self, circle): + self._canvas.circle( + circle.cx, circle.cy, circle.r, + fill=self._fill, + stroke=self._stroke + ) + + def drawPolyLine(self, polyline): + if self._stroke: + assert len(polyline.points) >= 2, 'Polyline must have 2 or more points' + head, tail = polyline.points[0:2], polyline.points[2:], + path = self._canvas.beginPath() + path.moveTo(head[0], head[1]) + for i in range(0, len(tail), 2): + path.lineTo(tail[i], tail[i+1]) + self._canvas.drawPath(path) + + def drawWedge(self, wedge): + centerx, centery, radius, startangledegrees, endangledegrees = \ + wedge.centerx, wedge.centery, wedge.radius, wedge.startangledegrees, wedge.endangledegrees + yradius, radius1, yradius1 = wedge._xtraRadii() + if yradius is None: yradius = radius + angle = endangledegrees-startangledegrees + path = self._canvas.beginPath() + if (radius1==0 or radius1 is None) and (yradius1==0 or yradius1 is None): + path.moveTo(centerx, centery) + path.arcTo(centerx-radius, centery-yradius, centerx+radius, centery+yradius, + startangledegrees, angle) + else: + path.arc(centerx-radius, centery-yradius, centerx+radius, centery+yradius, + startangledegrees, angle) + path.arcTo(centerx-radius1, centery-yradius1, centerx+radius1, centery+yradius1, + endangledegrees, -angle) + path.close() + self._canvas.drawPath(path, + fill=self._fill, + stroke=self._stroke) + + def drawEllipse(self, ellipse): + #need to convert to pdfgen's bounding box representation + x1 = ellipse.cx - ellipse.rx + x2 = ellipse.cx + ellipse.rx + y1 = ellipse.cy - ellipse.ry + y2 = ellipse.cy + ellipse.ry + self._canvas.ellipse(x1,y1,x2,y2,fill=self._fill,stroke=self._stroke) + + def drawPolygon(self, polygon): + assert len(polygon.points) >= 2, 'Polyline must have 2 or more points' + head, tail = polygon.points[0:2], polygon.points[2:], + path = self._canvas.beginPath() + path.moveTo(head[0], head[1]) + for i in range(0, len(tail), 2): + path.lineTo(tail[i], tail[i+1]) + path.close() + self._canvas.drawPath( + path, + stroke=self._stroke, + fill=self._fill + ) + + def drawString(self, stringObj): + if self._fill: + S = self._tracker.getState() + text_anchor, x, y, text = S['textAnchor'], stringObj.x,stringObj.y,stringObj.text + if not text_anchor in ['start','inherited']: + font, font_size = S['fontName'], S['fontSize'] + textLen = stringWidth(text, font,font_size) + if text_anchor=='end': + x = x-textLen + elif text_anchor=='middle': + x = x - textLen/2 + else: + raise ValueError, 'bad value for textAnchor '+str(text_anchor) + t = self._canvas.beginText(x,y) + t.textLine(text) + self._canvas.drawText(t) + + def drawPath(self, path): + from reportlab.graphics.shapes import _renderPath + pdfPath = self._canvas.beginPath() + drawFuncs = (pdfPath.moveTo, pdfPath.lineTo, pdfPath.curveTo, pdfPath.close) + isClosed = _renderPath(path, drawFuncs) + if isClosed: + fill = self._fill + else: + fill = 0 + if path.isClipPath: + self._canvas.clipPath(pdfPath, fill=fill, stroke=self._stroke) + else: + self._canvas.drawPath(pdfPath, + fill=fill, + stroke=self._stroke) + + def applyStateChanges(self, delta, newState): + """This takes a set of states, and outputs the PDF operators + needed to set those properties""" + for key, value in delta.items(): + if key == 'transform': + self._canvas.transform(value[0], value[1], value[2], + value[3], value[4], value[5]) + elif key == 'strokeColor': + #this has different semantics in PDF to SVG; + #we always have a color, and either do or do + #not apply it; in SVG one can have a 'None' color + if value is None: + self._stroke = 0 + else: + self._stroke = 1 + self._canvas.setStrokeColor(value) + elif key == 'strokeWidth': + self._canvas.setLineWidth(value) + elif key == 'strokeLineCap': #0,1,2 + self._canvas.setLineCap(value) + elif key == 'strokeLineJoin': + self._canvas.setLineJoin(value) +# elif key == 'stroke_dasharray': +# self._canvas.setDash(array=value) + elif key == 'strokeDashArray': + if value: + self._canvas.setDash(value) + else: + self._canvas.setDash() + elif key == 'fillColor': + #this has different semantics in PDF to SVG; + #we always have a color, and either do or do + #not apply it; in SVG one can have a 'None' color + if value is None: + self._fill = 0 + else: + self._fill = 1 + self._canvas.setFillColor(value) + elif key in ['fontSize', 'fontName']: + # both need setting together in PDF + # one or both might be in the deltas, + # so need to get whichever is missing + fontname = delta.get('fontName', self._canvas._fontname) + fontsize = delta.get('fontSize', self._canvas._fontsize) + self._canvas.setFont(fontname, fontsize) + +from reportlab.platypus import Flowable + +class GraphicsFlowable(Flowable): + """Flowable wrapper around a Pingo drawing""" + def __init__(self, drawing): + self.drawing = drawing + self.width = self.drawing.width + self.height = self.drawing.height + + def draw(self): + draw(self.drawing, self.canv, 0, 0) + +def drawToFile(d, fn, msg="", showBoundary=rl_config._unset_, autoSize=1): + """Makes a one-page PDF with just the drawing. + + If autoSize=1, the PDF will be the same size as + the drawing; if 0, it will place the drawing on + an A4 page with a title above it - possibly overflowing + if too big.""" + c = Canvas(fn) + c.setFont('Times-Roman', 36) + c.drawString(80, 750, msg) + c.setTitle(msg) + + if autoSize: + c.setPageSize((d.width, d.height)) + draw(d, c, 0, 0, showBoundary=showBoundary) + else: + #show with a title + c.setFont('Times-Roman', 12) + y = 740 + i = 1 + y = y - d.height + draw(d, c, 80, y, showBoundary=showBoundary) + + c.showPage() + c.save() + if sys.platform=='mac' and not hasattr(fn, "write"): + try: + import macfs, macostools + macfs.FSSpec(fn).SetCreatorType("CARO", "PDF ") + macostools.touched(fn) + except: + pass + + +def drawToString(d, msg="", showBoundary=rl_config._unset_,autoSize=1): + "Returns a PDF as a string in memory, without touching the disk" + s = getStringIO() + drawToFile(d, s, msg=msg, showBoundary=showBoundary,autoSize=autoSize) + return s.getvalue() + + +######################################################### +# +# test code. First, defin a bunch of drawings. +# Routine to draw them comes at the end. +# +######################################################### + + +def test(): + c = Canvas('renderPDF.pdf') + c.setFont('Times-Roman', 36) + c.drawString(80, 750, 'Graphics Test') + + # print all drawings and their doc strings from the test + # file + + #grab all drawings from the test module + from reportlab.graphics import testshapes + drawings = [] + for funcname in dir(testshapes): + if funcname[0:10] == 'getDrawing': + drawing = eval('testshapes.' + funcname + '()') #execute it + docstring = eval('testshapes.' + funcname + '.__doc__') + drawings.append((drawing, docstring)) + + #print in a loop, with their doc strings + c.setFont('Times-Roman', 12) + y = 740 + i = 1 + for (drawing, docstring) in drawings: + assert (docstring is not None), "Drawing %d has no docstring!" % i + if y < 300: #allows 5-6 lines of text + c.showPage() + y = 740 + # draw a title + y = y - 30 + c.setFont('Times-BoldItalic',12) + c.drawString(80, y, 'Drawing %d' % i) + c.setFont('Times-Roman',12) + y = y - 14 + textObj = c.beginText(80, y) + textObj.textLines(docstring) + c.drawText(textObj) + y = textObj.getY() + y = y - drawing.height + draw(drawing, c, 80, y) + i = i + 1 + if y!=740: c.showPage() + + c.save() + print 'saved renderPDF.pdf' + +##def testFlowable(): +## """Makes a platypus document""" +## from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer +## from reportlab.lib.styles import getSampleStyleSheet +## styles = getSampleStyleSheet() +## styNormal = styles['Normal'] +## +## doc = SimpleDocTemplate('test_flowable.pdf') +## story = [] +## story.append(Paragraph("This sees is a drawing can work as a flowable", styNormal)) +## +## import testdrawings +## drawings = [] +## +## for funcname in dir(testdrawings): +## if funcname[0:10] == 'getDrawing': +## drawing = eval('testdrawings.' + funcname + '()') #execute it +## docstring = eval('testdrawings.' + funcname + '.__doc__') +## story.append(Paragraph(docstring, styNormal)) +## story.append(Spacer(18,18)) +## story.append(drawing) +## story.append(Spacer(36,36)) +## +## doc.build(story) +## print 'saves test_flowable.pdf' + +if __name__=='__main__': + test() + #testFlowable() diff --git a/bin/reportlab/graphics/renderPM.py b/bin/reportlab/graphics/renderPM.py new file mode 100644 index 00000000000..4da2cffc015 --- /dev/null +++ b/bin/reportlab/graphics/renderPM.py @@ -0,0 +1,631 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history www.reportlab.co.uk/rl-cgi/viewcvs.cgi/rlextra/graphics/Csrc/renderPM/renderP.py +__version__=''' $Id$ ''' +"""Usage: + from reportlab.graphics import renderPM + renderPM.drawToFile(drawing,filename,fmt='GIF',configPIL={....}) +Other functions let you create a PM drawing as string or into a PM buffer. +Execute the script to see some test drawings.""" + +from reportlab.graphics.shapes import * +from reportlab.graphics.renderbase import StateTracker, getStateDelta +from reportlab.pdfbase.pdfmetrics import getFont +from math import sin, cos, pi, ceil +from reportlab.lib.utils import getStringIO, open_and_read +from reportlab import rl_config + +class RenderPMError(Exception): + pass + +import string, os, sys + +try: + import _renderPM +except ImportError, errMsg: + raise ImportError, "No module named _renderPM\n" + \ + (str(errMsg)!='No module named _renderPM' and "it may be the wrong version or badly installed!" or + "see http://www.reportlab.org/rl_addons.html") + +from types import TupleType, ListType +_SeqTypes = (TupleType,ListType) + +def _getImage(): + try: + from PIL import Image + except ImportError: + import Image + return Image + +def Color2Hex(c): + #assert isinstance(colorobj, colors.Color) #these checks don't work well RGB + if c: return ((0xFF&int(255*c.red)) << 16) | ((0xFF&int(255*c.green)) << 8) | (0xFF&int(255*c.blue)) + return c + +# the main entry point for users... +def draw(drawing, canvas, x, y, showBoundary=rl_config._unset_): + """As it says""" + R = _PMRenderer() + R.draw(drawing, canvas, x, y, showBoundary=showBoundary) + +from reportlab.graphics.renderbase import Renderer +class _PMRenderer(Renderer): + """This draws onto a pix map image. It needs to be a class + rather than a function, as some image-specific state tracking is + needed outside of the state info in the SVG model.""" + + def __init__(self): + self._tracker = StateTracker() + + def pop(self): + self._tracker.pop() + self.applyState() + + def push(self,node): + deltas = getStateDelta(node) + self._tracker.push(deltas) + self.applyState() + + def applyState(self): + s = self._tracker.getState() + self._canvas.ctm = s['ctm'] + self._canvas.strokeWidth = s['strokeWidth'] + self._canvas.strokeColor = Color2Hex(s['strokeColor']) + self._canvas.lineCap = s['strokeLineCap'] + self._canvas.lineJoin = s['strokeLineJoin'] + da = s['strokeDashArray'] + da = da and (0,da) or None + self._canvas.dashArray = da + self._canvas.fillColor = Color2Hex(s['fillColor']) + self._canvas.setFont(s['fontName'], s['fontSize']) + + def initState(self,x,y): + deltas = STATE_DEFAULTS.copy() + deltas['transform'] = self._canvas._baseCTM[0:4]+(x,y) + self._tracker.push(deltas) + self.applyState() + + def drawNode(self, node): + """This is the recursive method called for each node + in the tree""" + + #apply state changes + self.push(node) + + #draw the object, or recurse + self.drawNodeDispatcher(node) + + # restore the state + self.pop() + + def drawRect(self, rect): + c = self._canvas + if rect.rx == rect.ry == 0: + #plain old rectangle, draw clockwise (x-axis to y-axis) direction + c.rect(rect.x,rect.y, rect.width, rect.height) + else: + c.roundRect(rect.x,rect.y, rect.width, rect.height, rect.rx, rect.ry) + + def drawLine(self, line): + self._canvas.line(line.x1,line.y1,line.x2,line.y2) + + def drawImage(self, image): + if image.path and os.path.exists(image.path): + if type(image.path) is type(''): + im = _getImage().open(image.path).convert('RGB') + else: + im = image.path.convert('RGB') + srcW, srcH = im.size + dstW, dstH = image.width, image.height + if dstW is None: dstW = srcW + if dstH is None: dstH = srcH + self._canvas._aapixbuf( + image.x, image.y, dstW, dstH, + im.tostring(), srcW, srcH, 3, + ) + + def drawCircle(self, circle): + c = self._canvas + c.circle(circle.cx,circle.cy, circle.r) + c.fillstrokepath() + + def drawPolyLine(self, polyline, _doClose=0): + P = polyline.points + assert len(P) >= 2, 'Polyline must have 1 or more points' + c = self._canvas + c.pathBegin() + c.moveTo(P[0], P[1]) + for i in range(2, len(P), 2): + c.lineTo(P[i], P[i+1]) + if _doClose: + c.pathClose() + c.pathFill() + c.pathStroke() + + def drawEllipse(self, ellipse): + c=self._canvas + c.ellipse(ellipse.cx, ellipse.cy, ellipse.rx,ellipse.ry) + c.fillstrokepath() + + def drawPolygon(self, polygon): + self.drawPolyLine(polygon,_doClose=1) + + def drawString(self, stringObj): + fill = self._canvas.fillColor + if fill is not None: + S = self._tracker.getState() + text_anchor, x, y, text = S['textAnchor'], stringObj.x,stringObj.y,stringObj.text + if not text_anchor in ['start','inherited']: + font, font_size = S['fontName'], S['fontSize'] + textLen = stringWidth(text, font,font_size) + if text_anchor=='end': + x = x-textLen + elif text_anchor=='middle': + x = x - textLen/2 + else: + raise ValueError, 'bad value for textAnchor '+str(text_anchor) + self._canvas.drawString(x,y,text) + + def drawPath(self, path): + c = self._canvas + if path is EmptyClipPath: + del c._clipPaths[-1] + if c._clipPaths: + P = c._clipPaths[-1] + icp = P.isClipPath + P.isClipPath = 1 + self.drawPath(P) + P.isClipPath = icp + else: + c.clipPathClear() + return + c.pathBegin() + drawFuncs = (c.moveTo, c.lineTo, c.curveTo, c.pathClose) + from reportlab.graphics.shapes import _renderPath + isClosed = _renderPath(path, drawFuncs) + if path.isClipPath: + c.clipPathSet() + c._clipPaths.append(path) + else: + if isClosed: c.pathFill() + c.pathStroke() + +def _setFont(gs,fontName,fontSize): + try: + gs.setFont(fontName,fontSize) + except _renderPM.Error, errMsg: + if errMsg.args[0]!="Can't find font!": raise + #here's where we try to add a font to the canvas + try: + f = getFont(fontName) + if _renderPM._version<='0.98': #added reader arg in 0.99 + _renderPM.makeT1Font(fontName,f.face.findT1File(),f.encoding.vector) + else: + _renderPM.makeT1Font(fontName,f.face.findT1File(),f.encoding.vector,open_and_read) + except: + s1, s2 = map(str,sys.exc_info()[:2]) + raise RenderPMError, "Can't setFont(%s) missing the T1 files?\nOriginally %s: %s" % (fontName,s1,s2) + gs.setFont(fontName,fontSize) + +def _convert2pilp(im): + Image = _getImage() + return im.convert("P", dither=Image.NONE, palette=Image.ADAPTIVE) + +def _saveAsPICT(im,fn,fmt,transparent=None): + im = _convert2pilp(im) + cols, rows = im.size + #s = _renderPM.pil2pict(cols,rows,im.tostring(),im.im.getpalette(),transparent is not None and Color2Hex(transparent) or -1) + s = _renderPM.pil2pict(cols,rows,im.tostring(),im.im.getpalette()) + if not hasattr(fn,'write'): + open(os.path.splitext(fn)[0]+'.'+string.lower(fmt),'wb').write(s) + if os.name=='mac': + from reportlab.lib.utils import markfilename + markfilename(fn,ext='PICT') + else: + fn.write(s) + +BEZIER_ARC_MAGIC = 0.5522847498 #constant for drawing circular arcs w/ Beziers +class PMCanvas: + def __init__(self,w,h,dpi=72,bg=0xffffff,configPIL=None): + '''configPIL dict is passed to image save method''' + scale = dpi/72.0 + w = int(w*scale+0.5) + h = int(h*scale+0.5) + self.__dict__['_gs'] = _renderPM.gstate(w,h,bg=bg) + self.__dict__['_bg'] = bg + self.__dict__['_baseCTM'] = (scale,0,0,scale,0,0) + self.__dict__['_clipPaths'] = [] + self.__dict__['configPIL'] = configPIL + self.__dict__['_dpi'] = dpi + self.ctm = self._baseCTM + + def _drawTimeResize(self,w,h,bg=None): + if bg is None: bg = self._bg + self._drawing.width, self._drawing.height = w, h + A = {'ctm':None, 'strokeWidth':None, 'strokeColor':None, 'lineCap':None, 'lineJoin':None, 'dashArray':None, 'fillColor':None} + gs = self._gs + fN,fS = gs.fontName, gs.fontSize + for k in A.keys(): + A[k] = getattr(gs,k) + del gs, self._gs + gs = self.__dict__['_gs'] = _renderPM.gstate(w,h,bg=bg) + for k in A.keys(): + setattr(self,k,A[k]) + gs.setFont(fN,fS) + + def toPIL(self): + im = _getImage().new('RGB', size=(self._gs.width, self._gs.height)) + im.fromstring(self._gs.pixBuf) + return im + + def saveToFile(self,fn,fmt=None): + im = self.toPIL() + if fmt is None: + if type(fn) is not StringType: + raise ValueError, "Invalid type '%s' for fn when fmt is None" % type(fn) + fmt = os.path.splitext(fn)[1] + if fmt.startswith('.'): fmt = fmt[1:] + configPIL = self.configPIL or {} + fmt = string.upper(fmt) + if fmt in ['GIF']: + im = _convert2pilp(im) + elif fmt in ['PCT','PICT']: + return _saveAsPICT(im,fn,fmt,transparent=configPIL.get('transparent',None)) + elif fmt in ['PNG','TIFF','BMP', 'PPM', 'TIF']: + if fmt=='TIF': fmt = 'TIFF' + if fmt=='PNG': + try: + from PIL import PngImagePlugin + except ImportError: + import PngImagePlugin + elif fmt=='BMP': + try: + from PIL import BmpImagePlugin + except ImportError: + import BmpImagePlugin + elif fmt in ('JPG','JPEG'): + fmt = 'JPEG' + else: + raise RenderPMError,"Unknown image kind %s" % fmt + if fmt=='TIFF': + tc = configPIL.get('transparent',None) + if tc: + from PIL import ImageChops, Image + T = 768*[0] + for o, c in zip((0,256,512), tc.bitmap_rgb()): + T[o+c] = 255 + #if type(fn) is type(''): ImageChops.invert(im.point(T).convert('L').point(255*[0]+[255])).save(fn+'_mask.gif','GIF') + im = Image.merge('RGBA', im.split()+(ImageChops.invert(im.point(T).convert('L').point(255*[0]+[255])),)) + #if type(fn) is type(''): im.save(fn+'_masked.gif','GIF') + for a,d in ('resolution',self._dpi),('resolution unit','inch'): + configPIL[a] = configPIL.get(a,d) + apply(im.save,(fn,fmt),configPIL) + if not hasattr(fn,'write') and os.name=='mac': + from reportlab.lib.utils import markfilename + markfilename(fn,ext=fmt) + + def saveToString(self,fmt='GIF'): + s = getStringIO() + self.saveToFile(s,fmt=fmt) + return s.getvalue() + + def _saveToBMP(self,f): + ''' + Niki Spahiev, <niki@vintech.bg>, asserts that this is a respectable way to get BMP without PIL + f is a file like object to which the BMP is written + ''' + import struct + gs = self._gs + pix, width, height = gs.pixBuf, gs.width, gs.height + f.write(struct.pack('=2sLLLLLLhh24x','BM',len(pix)+54,0,54,40,width,height,1,24)) + rowb = width * 3 + for o in range(len(pix),0,-rowb): + f.write(pix[o-rowb:o]) + f.write( '\0' * 14 ) + + def setFont(self,fontName,fontSize,leading=None): + _setFont(self._gs,fontName,fontSize) + + def __setattr__(self,name,value): + setattr(self._gs,name,value) + + def __getattr__(self,name): + return getattr(self._gs,name) + + def fillstrokepath(self,stroke=1,fill=1): + if fill: self.pathFill() + if stroke: self.pathStroke() + + def _bezierArcSegmentCCW(self, cx,cy, rx,ry, theta0, theta1): + """compute the control points for a bezier arc with theta1-theta0 <= 90. + Points are computed for an arc with angle theta increasing in the + counter-clockwise (CCW) direction. returns a tuple with starting point + and 3 control points of a cubic bezier curve for the curvto opertator""" + + # Requires theta1 - theta0 <= 90 for a good approximation + assert abs(theta1 - theta0) <= 90 + cos0 = cos(pi*theta0/180.0) + sin0 = sin(pi*theta0/180.0) + x0 = cx + rx*cos0 + y0 = cy + ry*sin0 + + cos1 = cos(pi*theta1/180.0) + sin1 = sin(pi*theta1/180.0) + + x3 = cx + rx*cos1 + y3 = cy + ry*sin1 + + dx1 = -rx * sin0 + dy1 = ry * cos0 + + #from pdfgeom + halfAng = pi*(theta1-theta0)/(2.0 * 180.0) + k = abs(4.0 / 3.0 * (1.0 - cos(halfAng) ) /(sin(halfAng)) ) + x1 = x0 + dx1 * k + y1 = y0 + dy1 * k + + dx2 = -rx * sin1 + dy2 = ry * cos1 + + x2 = x3 - dx2 * k + y2 = y3 - dy2 * k + return ((x0,y0), ((x1,y1), (x2,y2), (x3,y3)) ) + + def bezierArcCCW(self, cx,cy, rx,ry, theta0, theta1): + """return a set of control points for Bezier approximation to an arc + with angle increasing counter clockwise. No requirement on |theta1-theta0| <= 90 + However, it must be true that theta1-theta0 > 0.""" + + # I believe this is also clockwise + # pretty much just like Robert Kern's pdfgeom.BezierArc + angularExtent = theta1 - theta0 + # break down the arc into fragments of <=90 degrees + if abs(angularExtent) <= 90.0: # we just need one fragment + angleList = [(theta0,theta1)] + else: + Nfrag = int( ceil( abs(angularExtent)/90.) ) + fragAngle = float(angularExtent)/ Nfrag # this could be negative + angleList = [] + for ii in range(Nfrag): + a = theta0 + ii * fragAngle + b = a + fragAngle # hmm.. is I wonder if this is precise enought + angleList.append((a,b)) + + ctrlpts = [] + for (a,b) in angleList: + if not ctrlpts: # first time + [(x0,y0), pts] = self._bezierArcSegmentCCW(cx,cy, rx,ry, a,b) + ctrlpts.append(pts) + else: + [(tmpx,tmpy), pts] = self._bezierArcSegmentCCW(cx,cy, rx,ry, a,b) + ctrlpts.append(pts) + return ((x0,y0), ctrlpts) + + def addEllipsoidalArc(self, cx,cy, rx, ry, ang1, ang2): + """adds an ellisesoidal arc segment to a path, with an ellipse centered + on cx,cy and with radii (major & minor axes) rx and ry. The arc is + drawn in the CCW direction. Requires: (ang2-ang1) > 0""" + + ((x0,y0), ctrlpts) = self.bezierArcCCW(cx,cy, rx,ry,ang1,ang2) + + self.lineTo(x0,y0) + for ((x1,y1), (x2,y2),(x3,y3)) in ctrlpts: + self.curveTo(x1,y1,x2,y2,x3,y3) + + def drawCentredString(self, x, y, text, text_anchor='middle'): + if self.fillColor is not None: + textLen = stringWidth(text, self.fontName,self.fontSize) + if text_anchor=='end': + x = x-textLen + elif text_anchor=='middle': + x = x - textLen/2 + self.drawString(x,y,text) + + def drawRightString(self, text, x, y): + self.drawCentredString(text,x,y,text_anchor='end') + + def line(self,x1,y1,x2,y2): + if self.strokeColor is not None: + self.pathBegin() + self.moveTo(x1,y1) + self.lineTo(x2,y2) + self.pathStroke() + + def rect(self,x,y,width,height,stroke=1,fill=1): + self.pathBegin() + self.moveTo(x, y) + self.lineTo(x+width, y) + self.lineTo(x+width, y + height) + self.lineTo(x, y + height) + self.pathClose() + self.fillstrokepath(stroke=stroke,fill=fill) + + def roundRect(self, x, y, width, height, rx,ry): + """rect(self, x, y, width, height, rx,ry): + Draw a rectangle if rx or rx and ry are specified the corners are + rounded with ellipsoidal arcs determined by rx and ry + (drawn in the counter-clockwise direction)""" + if rx==0: rx = ry + if ry==0: ry = rx + x2 = x + width + y2 = y + height + self.pathBegin() + self.moveTo(x+rx,y) + self.addEllipsoidalArc(x2-rx, y+ry, rx, ry, 270, 360 ) + self.addEllipsoidalArc(x2-rx, y2-ry, rx, ry, 0, 90) + self.addEllipsoidalArc(x+rx, y2-ry, rx, ry, 90, 180) + self.addEllipsoidalArc(x+rx, y+ry, rx, ry, 180, 270) + self.pathClose() + self.fillstrokepath() + + def circle(self, cx, cy, r): + "add closed path circle with center cx,cy and axes r: counter-clockwise orientation" + self.ellipse(cx,cy,r,r) + + def ellipse(self, cx,cy,rx,ry): + """add closed path ellipse with center cx,cy and axes rx,ry: counter-clockwise orientation + (remember y-axis increases downward) """ + self.pathBegin() + # first segment + x0 = cx + rx # (x0,y0) start pt + y0 = cy + + x3 = cx # (x3,y3) end pt of arc + y3 = cy-ry + + x1 = cx+rx + y1 = cy-ry*BEZIER_ARC_MAGIC + + x2 = x3 + rx*BEZIER_ARC_MAGIC + y2 = y3 + self.moveTo(x0, y0) + self.curveTo(x1,y1,x2,y2,x3,y3) + # next segment + x0 = x3 + y0 = y3 + + x3 = cx-rx + y3 = cy + + x1 = cx-rx*BEZIER_ARC_MAGIC + y1 = cy-ry + + x2 = x3 + y2 = cy- ry*BEZIER_ARC_MAGIC + self.curveTo(x1,y1,x2,y2,x3,y3) + # next segment + x0 = x3 + y0 = y3 + + x3 = cx + y3 = cy+ry + + x1 = cx-rx + y1 = cy+ry*BEZIER_ARC_MAGIC + + x2 = cx -rx*BEZIER_ARC_MAGIC + y2 = cy+ry + self.curveTo(x1,y1,x2,y2,x3,y3) + #last segment + x0 = x3 + y0 = y3 + + x3 = cx+rx + y3 = cy + + x1 = cx+rx*BEZIER_ARC_MAGIC + y1 = cy+ry + + x2 = cx+rx + y2 = cy+ry*BEZIER_ARC_MAGIC + self.curveTo(x1,y1,x2,y2,x3,y3) + self.pathClose() + + def saveState(self): + '''do nothing for compatibility''' + pass + + def setFillColor(self,aColor): + self.fillColor = Color2Hex(aColor) + + def setStrokeColor(self,aColor): + self.strokeColor = Color2Hex(aColor) + + restoreState = saveState + + # compatibility routines + def setLineCap(self,cap): + self.lineCap = cap + + def setLineWidth(self,width): + self.strokeWidth = width + +def drawToPMCanvas(d, dpi=72, bg=0xffffff, configPIL=None, showBoundary=rl_config._unset_): + c = PMCanvas(d.width, d.height, dpi=dpi, bg=bg, configPIL=configPIL) + draw(d, c, 0, 0, showBoundary=showBoundary) + return c + +def drawToPIL(d, dpi=72, bg=0xffffff, configPIL=None, showBoundary=rl_config._unset_): + return drawToPMCanvas(d, dpi=dpi, bg=bg, configPIL=configPIL, showBoundary=showBoundary).toPIL() + +def drawToPILP(d, dpi=72, bg=0xffffff, configPIL=None, showBoundary=rl_config._unset_): + Image = _getImage() + im = drawToPIL(d, dpi=dpi, bg=bg, configPIL=configPIL, showBoundary=showBoundary) + return im.convert("P", dither=Image.NONE, palette=Image.ADAPTIVE) + +def drawToFile(d,fn,fmt='GIF', dpi=72, bg=0xffffff, configPIL=None, showBoundary=rl_config._unset_): + '''create a pixmap and draw drawing, d to it then save as a file + configPIL dict is passed to image save method''' + c = drawToPMCanvas(d, dpi=dpi, bg=bg, configPIL=configPIL, showBoundary=showBoundary) + c.saveToFile(fn,fmt) + +def drawToString(d,fmt='GIF', dpi=72, bg=0xffffff, configPIL=None, showBoundary=rl_config._unset_): + s = getStringIO() + drawToFile(d,s,fmt=fmt, dpi=dpi, bg=bg, configPIL=configPIL) + return s.getvalue() + +save = drawToFile + +def test(): + def ext(x): + if x=='tiff': x='tif' + return x + #grab all drawings from the test module and write out. + #make a page of links in HTML to assist viewing. + import os + from reportlab.graphics import testshapes + getAllTestDrawings = testshapes.getAllTestDrawings + drawings = [] + if not os.path.isdir('pmout'): + os.mkdir('pmout') + htmlTop = """<html><head><title>renderPM output results</title></head> + <body> + <h1>renderPM results of output</h1> + """ + htmlBottom = """</body> + </html> + """ + html = [htmlTop] + + i = 0 + #print in a loop, with their doc strings + for (drawing, docstring, name) in getAllTestDrawings(doTTF=hasattr(_renderPM,'ft_get_face')): + fnRoot = 'renderPM%d' % i + if 1 or i==10: + w = int(drawing.width) + h = int(drawing.height) + html.append('<hr><h2>Drawing %s %d</h2>\n<pre>%s</pre>' % (name, i, docstring)) + + for k in ['gif','tiff', 'png', 'jpg', 'pct']: + if k in ['gif','png','jpg','pct']: + html.append('<p>%s format</p>\n' % string.upper(k)) + try: + filename = '%s.%s' % (fnRoot, ext(k)) + fullpath = os.path.join('pmout', filename) + if os.path.isfile(fullpath): + os.remove(fullpath) + if k=='pct': + from reportlab.lib.colors import white + drawToFile(drawing,fullpath,fmt=k,configPIL={'transparent':white}) + else: + drawToFile(drawing,fullpath,fmt=k) + if k in ['gif','png','jpg']: + html.append('<img src="%s" border="1"><br>\n' % filename) + print 'wrote',fullpath + except AttributeError: + print 'Problem drawing %s file'%k + raise + if os.environ.get('RL_NOEPSPREVIEW','0')=='1': drawing.__dict__['preview'] = 0 + drawing.save(formats=['eps','pdf'],outDir='pmout',fnRoot=fnRoot) + i = i + 1 + #if i==10: break + html.append(htmlBottom) + htmlFileName = os.path.join('pmout', 'index.html') + open(htmlFileName, 'w').writelines(html) + if sys.platform=='mac': + from reportlab.lib.utils import markfilename + markfilename(htmlFileName,ext='HTML') + print 'wrote %s' % htmlFileName + +if __name__=='__main__': + test() diff --git a/bin/reportlab/graphics/renderPS.py b/bin/reportlab/graphics/renderPS.py new file mode 100644 index 00000000000..e117bb7fc89 --- /dev/null +++ b/bin/reportlab/graphics/renderPS.py @@ -0,0 +1,859 @@ +#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/renderPS.py +__version__=''' $Id$ ''' +import string, types +from reportlab.pdfbase.pdfmetrics import getFont, stringWidth # for font info +from reportlab.lib.utils import fp_str, getStringIO +from reportlab.lib.colors import black +from reportlab.graphics.renderbase import StateTracker, getStateDelta +from reportlab.graphics.shapes import STATE_DEFAULTS +import math +from types import StringType +from operator import getitem +from reportlab import rl_config + + +# we need to create encoding vectors for each font we use, or they will +# come out in Adobe's old StandardEncoding, which NOBODY uses. +PS_WinAnsiEncoding=""" +/RE { %def + findfont begin + currentdict dup length dict begin + { %forall + 1 index /FID ne { def } { pop pop } ifelse + } forall + /FontName exch def dup length 0 ne { %if + /Encoding Encoding 256 array copy def + 0 exch { %forall + dup type /nametype eq { %ifelse + Encoding 2 index 2 index put + pop 1 add + }{ %else + exch pop + } ifelse + } forall + } if pop + currentdict dup end end + /FontName get exch definefont pop +} bind def + +/WinAnsiEncoding [ + 39/quotesingle 96/grave 128/euro 130/quotesinglbase/florin/quotedblbase + /ellipsis/dagger/daggerdbl/circumflex/perthousand + /Scaron/guilsinglleft/OE 145/quoteleft/quoteright + /quotedblleft/quotedblright/bullet/endash/emdash + /tilde/trademark/scaron/guilsinglright/oe/dotlessi + 159/Ydieresis 164/currency 166/brokenbar 168/dieresis/copyright + /ordfeminine 172/logicalnot 174/registered/macron/ring + 177/plusminus/twosuperior/threesuperior/acute/mu + 183/periodcentered/cedilla/onesuperior/ordmasculine + 188/onequarter/onehalf/threequarters 192/Agrave/Aacute + /Acircumflex/Atilde/Adieresis/Aring/AE/Ccedilla + /Egrave/Eacute/Ecircumflex/Edieresis/Igrave/Iacute + /Icircumflex/Idieresis/Eth/Ntilde/Ograve/Oacute + /Ocircumflex/Otilde/Odieresis/multiply/Oslash + /Ugrave/Uacute/Ucircumflex/Udieresis/Yacute/Thorn + /germandbls/agrave/aacute/acircumflex/atilde/adieresis + /aring/ae/ccedilla/egrave/eacute/ecircumflex + /edieresis/igrave/iacute/icircumflex/idieresis + /eth/ntilde/ograve/oacute/ocircumflex/otilde + /odieresis/divide/oslash/ugrave/uacute/ucircumflex + /udieresis/yacute/thorn/ydieresis +] def + +""" + +class PSCanvas: + def __init__(self,size=(300,300), PostScriptLevel=2): + self.width, self.height = size + self.code = [] + self._sep = '\n' + self._strokeColor = self._fillColor = self._lineWidth = \ + self._font = self._fontSize = self._lineCap = \ + self._lineJoin = self._color = None + + + self._fontsUsed = [] # track them as we go + + self.setFont(STATE_DEFAULTS['fontName'],STATE_DEFAULTS['fontSize']) + self.setStrokeColor(STATE_DEFAULTS['strokeColor']) + self.setLineCap(2) + self.setLineJoin(0) + self.setLineWidth(1) + + self.PostScriptLevel=PostScriptLevel + + def comment(self,msg): + self.code.append('%'+msg) + + def drawImage(self, image, x1,y1, x2=None,y2=None): # Postscript Level2 version + # select between postscript level 1 or level 2 + if PostScriptLevel==1: + self._drawImageLevel1(image, x1,y1, x2=None,y2=None) + elif PostScriptLevel == 2 : + self._drawImageLevel2(image, x1,y1, x2=None,y2=None) + else : + raise 'PostScriptLevelException' + + def clear(self): + self.code.append('showpage') # ugh, this makes no sense oh well. + + def save(self,f=None): + if not hasattr(f,'write'): + file = open(f,'wb') + else: + file = f + if self.code[-1]!='showpage': self.clear() + self.code.insert(0,'''\ +%%!PS-Adobe-3.0 EPSF-3.0 +%%%%BoundingBox: 0 0 %d %d +%%%% Initialization: +/m {moveto} bind def +/l {lineto} bind def +/c {curveto} bind def + +%s +''' % (self.width,self.height, PS_WinAnsiEncoding)) + + # for each font used, reencode the vectors + fontReencode = [] + for fontName in self._fontsUsed: + fontReencode.append('WinAnsiEncoding /%s /%s RE' % (fontName, fontName)) + self.code.insert(1, string.join(fontReencode, self._sep)) + + file.write(string.join(self.code,self._sep)) + if file is not f: + file.close() + from reportlab.lib.utils import markfilename + markfilename(f,creatorcode='XPR3',filetype='EPSF') + + def saveState(self): + self.code.append('gsave') + + def restoreState(self): + self.code.append('grestore') + + def stringWidth(self, s, font=None, fontSize=None): + """Return the logical width of the string if it were drawn + in the current font (defaults to self.font).""" + font = font or self._font + fontSize = fontSize or self._fontSize + return stringWidth(s, font, fontSize) + + def setLineCap(self,v): + if self._lineCap!=v: + self._lineCap = v + self.code.append('%d setlinecap'%v) + + def setLineJoin(self,v): + if self._lineJoin!=v: + self._lineJoin = v + self.code.append('%d setlinejoin'%v) + + def setDash(self, array=[], phase=0): + """Two notations. pass two numbers, or an array and phase""" + # copied and modified from reportlab.canvas + psoperation = "setdash" + if type(array) == types.IntType or type(array) == types.FloatType: + self._code.append('[%s %s] 0 %s' % (array, phase, psoperation)) + elif type(array) == types.ListType or type(array) == types.TupleType: + assert phase >= 0, "phase is a length in user space" + textarray = string.join(map(str, array)) + self.code.append('[%s] %s %s' % (textarray, phase, psoperation)) + + def setStrokeColor(self, color): + self._strokeColor = color + self.setColor(color) + + def setColor(self, color): + if self._color!=color: + self._color = color + if color: + if hasattr(color, "cyan"): + self.code.append('%s setcmykcolor' % fp_str(color.cyan, color.magenta, color.yellow, color.black)) + else: + self.code.append('%s setrgbcolor' % fp_str(color.red, color.green, color.blue)) + + def setFillColor(self, color): + self._fillColor = color + self.setColor(color) + + def setLineWidth(self, width): + if width != self._lineWidth: + self._lineWidth = width + self.code.append('%s setlinewidth' % width) + + def setFont(self,font,fontSize,leading=None): + if self._font!=font or self._fontSize!=fontSize: + self._fontCodeLoc = len(self.code) + self._font = font + self._fontSize = fontSize + self.code.append('') + + def line(self, x1, y1, x2, y2): + if self._strokeColor != None: + self.code.append('%s m %s l stroke' % (fp_str(x1, y1), fp_str(x2, y2))) + + def _escape(self, s): + ''' + return a copy of string s with special characters in postscript strings + escaped with backslashes. + Have not handled characters that are converted normally in python strings + i.e. \n -> newline + ''' + str = string.replace(s, chr(0x5C), r'\\' ) + str = string.replace(str, '(', '\(' ) + str = string.replace(str, ')', '\)') + return str + + def drawString(self, x, y, s, angle=0): + if self._fillColor != None: + if not self.code[self._fontCodeLoc]: + psName = getFont(self._font).face.name + self.code[self._fontCodeLoc]='(%s) findfont %s scalefont setfont' % (psName,fp_str(self._fontSize)) + if psName not in self._fontsUsed: + self._fontsUsed.append(psName) + self.setColor(self._fillColor) + s = self._escape(s) +## before inverting... +## if angle == 0 : # do special case of angle = 0 first. Avoids a bunch of gsave/grestore ops +## self.code.append('%s m 1 -1 scale (%s) show 1 -1 scale' % (fp_str(x,y),s)) +## else : # general case, rotated text +## self.code.append('gsave %s %s translate %s rotate' % (x,y,angle)) +## self.code.append('0 0 m 1 -1 scale (%s) show' % s) +## self.code.append('grestore') + if angle == 0 : # do special case of angle = 0 first. Avoids a bunch of gsave/grestore ops + self.code.append('%s m (%s) show ' % (fp_str(x,y),s)) + else : # general case, rotated text + self.code.append('gsave %s %s translate %s rotate' % (x,y,angle)) + self.code.append('0 0 m (%s) show' % s) + self.code.append('grestore') + + def drawCentredString(self, x, y, text, text_anchor='middle'): + if self.fillColor is not None: + textLen = stringWidth(text, self._font,self._fontSize) + if text_anchor=='end': + x = x-textLen + elif text_anchor=='middle': + x = x - textLen/2 + self.drawString(x,y,text) + + def drawRightString(self, text, x, y): + self.drawCentredString(text,x,y,text_anchor='end') + + def drawCurve(self, x1, y1, x2, y2, x3, y3, x4, y4, closed=0): + codeline = '%s m %s curveto' + data = (fp_str(x1, y1), fp_str(x2, y2, x3, y3, x4, y4)) + if self._fillColor != None: + self.setColor(self._fillColor) + self.code.append((codeline % data) + ' eofill') + if self._strokeColor != None: + self.setColor(self._strokeColor) + self.code.append((codeline % data) + + ((closed and ' closepath') or '') + + ' stroke') + + ######################################################################################## + + def rect(self, x1,y1, x2,y2, stroke=1, fill=1): + "Draw a rectangle between x1,y1, and x2,y2" + # Path is drawn in counter-clockwise direction" + + x1, x2 = min(x1,x2), max(x1, x2) # from piddle.py + y1, y2 = min(y1,y2), max(y1, y2) + self.polygon(((x1,y1),(x2,y1),(x2,y2),(x1,y2)), closed=1, stroke=stroke, fill = fill) + + def roundRect(self, x1,y1, x2,y2, rx=8, ry=8): + """Draw a rounded rectangle between x1,y1, and x2,y2, + with corners inset as ellipses with x radius rx and y radius ry. + These should have x1<x2, y1<y2, rx>0, and ry>0.""" + # Path is drawn in counter-clockwise direction + + x1, x2 = min(x1,x2), max(x1, x2) # from piddle.py + y1, y2 = min(y1,y2), max(y1, y2) + + # Note: arcto command draws a line from current point to beginning of arc + # save current matrix, translate to center of ellipse, scale by rx ry, and draw + # a circle of unit radius in counterclockwise dir, return to original matrix + # arguments are (cx, cy, rx, ry, startAngle, endAngle) + ellipsePath = 'matrix currentmatrix %s %s translate %s %s scale 0 0 1 %s %s arc setmatrix' + + # choice between newpath and moveTo beginning of arc + # go with newpath for precision, does this violate any assumptions in code??? + rrCode = ['newpath'] # Round Rect code path + # upper left corner ellipse is first + rrCode.append(ellipsePath % (x1+rx, y1+ry, rx, -ry, 90, 180)) + rrCode.append(ellipsePath % (x1+rx, y2-ry, rx, -ry, 180, 270)) + rrCode.append(ellipsePath % (x2-rx, y2-ry, rx, -ry, 270, 360)) + rrCode.append(ellipsePath % (x2-rx, y1+ry, rx, -ry, 0, 90) ) + rrCode.append('closepath') + + self._fillAndStroke(rrCode) + + def ellipse(self, x1,y1, x2,y2): + """Draw an orthogonal ellipse inscribed within the rectangle x1,y1,x2,y2. + These should have x1<x2 and y1<y2.""" + #Just invoke drawArc to actually draw the ellipse + self.drawArc(x1,y1, x2,y2) + + def circle(self, xc, yc, r): + self.ellipse(xc-r,yc-r, xc+r,yc+r) + + def drawArc(self, x1,y1, x2,y2, startAng=0, extent=360, fromcenter=0): + """Draw a partial ellipse inscribed within the rectangle x1,y1,x2,y2, + starting at startAng degrees and covering extent degrees. Angles + start with 0 to the right (+x) and increase counter-clockwise. + These should have x1<x2 and y1<y2.""" + #calculate centre of ellipse + #print "x1,y1,x2,y2,startAng,extent,fromcenter", x1,y1,x2,y2,startAng,extent,fromcenter + cx, cy = (x1+x2)/2.0, (y1+y2)/2.0 + rx, ry = (x2-x1)/2.0, (y2-y1)/2.0 + + codeline = self._genArcCode(x1, y1, x2, y2, startAng, extent) + + startAngleRadians = math.pi*startAng/180.0 + extentRadians = math.pi*extent/180.0 + endAngleRadians = startAngleRadians + extentRadians + + codelineAppended = 0 + + # fill portion + + if self._fillColor != None: + self.setColor(self._fillColor) + self.code.append(codeline) + codelineAppended = 1 + if self._strokeColor!=None: self.code.append('gsave') + self.lineTo(cx,cy) + self.code.append('eofill') + if self._strokeColor!=None: self.code.append('grestore') + + # stroke portion + if self._strokeColor != None: + # this is a bit hacked up. There is certainly a better way... + self.setColor(self._strokeColor) + (startx, starty) = (cx+rx*math.cos(startAngleRadians), cy+ry*math.sin(startAngleRadians)) + if not codelineAppended: + self.code.append(codeline) + if fromcenter: + # move to center + self.lineTo(cx,cy) + self.lineTo(startx, starty) + self.code.append('closepath') + self.code.append('stroke') + + def _genArcCode(self, x1, y1, x2, y2, startAng, extent): + "Calculate the path for an arc inscribed in rectangle defined by (x1,y1),(x2,y2)" + #calculate semi-minor and semi-major axes of ellipse + xScale = abs((x2-x1)/2.0) + yScale = abs((y2-y1)/2.0) + #calculate centre of ellipse + x, y = (x1+x2)/2.0, (y1+y2)/2.0 + + codeline = 'matrix currentmatrix %s %s translate %s %s scale 0 0 1 %s %s %s setmatrix' + + if extent >= 0: + arc='arc' + else: + arc='arcn' + data = (x,y, xScale, yScale, startAng, startAng+extent, arc) + + return codeline % data + + def polygon(self, p, closed=0, stroke=1, fill=1): + assert len(p) >= 2, 'Polygon must have 2 or more points' + + start = p[0] + p = p[1:] + + polyCode = [] + polyCode.append("%s m" % fp_str(start)) + for point in p: + polyCode.append("%s l" % fp_str(point)) + if closed: + polyCode.append("closepath") + + self._fillAndStroke(polyCode,stroke=stroke,fill=fill) + + def lines(self, lineList, color=None, width=None): + if self._strokeColor != None: + self._setColor(self._strokeColor) + codeline = '%s m %s l stroke' + for line in lineList: + self.code.append(codeline % (fp_str(line[0]),fp_str(line[1]))) + + def moveTo(self,x,y): + self.code.append('%s m' % fp_str(x, y)) + + def lineTo(self,x,y): + self.code.append('%s l' % fp_str(x, y)) + + def curveTo(self,x1,y1,x2,y2,x3,y3): + self.code.append('%s c' % fp_str(x1,y1,x2,y2,x3,y3)) + + def closePath(self): + self.code.append('closepath') + + def polyLine(self, p): + assert len(p) >= 1, 'Polyline must have 1 or more points' + if self._strokeColor != None: + self.setColor(self._strokeColor) + self.moveTo(p[0][0], p[0][1]) + for t in p[1:]: + self.lineTo(t[0], t[1]) + self.code.append('stroke') + + + def drawFigure(self, partList, closed=0): + figureCode = [] + first = 1 + + for part in partList: + op = part[0] + args = list(part[1:]) + + if op == figureLine: + if first: + first = 0 + figureCode.append("%s m" % fp_str(args[:2])) + else: + figureCode.append("%s l" % fp_str(args[:2])) + figureCode.append("%s l" % fp_str(args[2:])) + + elif op == figureArc: + first = 0 + x1,y1,x2,y2,startAngle,extent = args[:6] + figureCode.append(self._genArcCode(x1,y1,x2,y2,startAngle,extent)) + + elif op == figureCurve: + if first: + first = 0 + figureCode.append("%s m" % fp_str(args[:2])) + else: + figureCode.append("%s l" % fp_str(args[:2])) + figureCode.append("%s curveto" % fp_str(args[2:])) + else: + raise TypeError, "unknown figure operator: "+op + + if closed: + figureCode.append("closepath") + self._fillAndStroke(figureCode) + + def _fillAndStroke(self,code,clip=0,fill=1,stroke=1): + fill = self._fillColor and fill + stroke = self._strokeColor and stroke + if fill or stroke or clip: + self.code.extend(code) + if fill: + if stroke or clip: self.code.append("gsave") + self.setColor(self._fillColor) + self.code.append("eofill") + if stroke or clip: self.code.append("grestore") + if stroke: + if clip: self.code.append("gsave") + self.setColor(self._strokeColor) + self.code.append("stroke") + if clip: self.code.append("grestore") + if clip: + self.code.append("clip") + self.code.append("newpath") + + + def translate(self,x,y): + self.code.append('%s translate' % fp_str(x,y)) + + def scale(self,x,y): + self.code.append('%s scale' % fp_str(x,y)) + + def transform(self,a,b,c,d,e,f): + self.code.append('[%s] concat' % fp_str(a,b,c,d,e,f)) + + def _drawTimeResize(self,w,h): + '''if this is used we're probably in the wrong world''' + self.width, self.height = w, h + + ############################################################################################ + # drawImage(self. image, x1, y1, x2=None, y2=None) is now defined by either _drawImageLevel1 + # ._drawImageLevel2, the choice is made in .__init__ depending on option + def _drawImageLevel1(self, image, x1, y1, x2=None,y2=None): + # Postscript Level1 version available for fallback mode when Level2 doesn't work + """drawImage(self,image,x1,y1,x2=None,y2=None) : If x2 and y2 are ommitted, they are + calculated from image size. (x1,y1) is upper left of image, (x2,y2) is lower right of + image in piddle coordinates.""" + # For now let's start with 24 bit RGB images (following piddlePDF again) + print "Trying to drawImage in piddlePS" + component_depth = 8 + myimage = image.convert('RGB') + imgwidth, imgheight = myimage.size + if not x2: + x2 = imgwidth + x1 + if not y2: + y2 = y1 + imgheight + drawwidth = x2 - x1 + drawheight = y2 - y1 + print 'Image size (%d, %d); Draw size (%d, %d)' % (imgwidth, imgheight, drawwidth, drawheight) + # now I need to tell postscript how big image is + + # "image operators assume that they receive sample data from + # their data source in x-axis major index order. The coordinate + # of the lower-left corner of the first sample is (0,0), of the + # second (1,0) and so on" -PS2 ref manual p. 215 + # + # The ImageMatrix maps unit squre of user space to boundary of the source image + # + + # The CurrentTransformationMatrix (CTM) maps the unit square of + # user space to the rect...on the page that is to receive the + # image. A common ImageMatrix is [width 0 0 -height 0 height] + # (for a left to right, top to bottom image ) + + # first let's map the user coordinates start at offset x1,y1 on page + + self.code.extend([ + 'gsave', + '%s %s translate' % (x1,-y1 - drawheight), # need to start are lower left of image + '%s %s scale' % (drawwidth,drawheight), + '/scanline %d 3 mul string def' % imgwidth # scanline by multiples of image width + ]) + + # now push the dimensions and depth info onto the stack + # and push the ImageMatrix to map the source to the target rectangle (see above) + # finally specify source (PS2 pp. 225 ) and by exmample + self.code.extend([ + '%s %s %s' % (imgwidth, imgheight, component_depth), + '[%s %s %s %s %s %s]' % (imgwidth, 0, 0, -imgheight, 0, imgheight), + '{ currentfile scanline readhexstring pop } false 3', + 'colorimage ' + ]) + + # data source output--now we just need to deliver a hex encode + # series of lines of the right overall size can follow + # piddlePDF again + + rawimage = myimage.tostring() + assert(len(rawimage) == imgwidth*imgheight, 'Wrong amount of data for image') + #compressed = zlib.compress(rawimage) # no zlib at moment + hex_encoded = self._AsciiHexEncode(rawimage) + + # write in blocks of 78 chars per line + outstream = getStringIO(hex_encoded) + + dataline = outstream.read(78) + while dataline <> "": + self.code.append(dataline) + dataline= outstream.read(78) + self.code.append('% end of image data') # for clarity + self.code.append('grestore') # return coordinates to normal + + # end of drawImage + def _AsciiHexEncode(self, input): # also based on piddlePDF + "Helper function used by images" + output = getStringIO() + for char in input: + output.write('%02x' % ord(char)) + return output.getvalue() + + def _drawImageLevel2(self, image, x1,y1, x2=None,y2=None): # Postscript Level2 version + Image = import_Image() + if not Image: return + ### what sort of image are we to draw + if image.mode=='L' : + print 'found image.mode= L' + imBitsPerComponent = 8 + imNumComponents = 1 + myimage = image + elif image.mode == '1': + print 'found image.mode= 1' + myimage = image.convert('L') + imNumComponents = 1 + myimage = image + else : + myimage = image.convert('RGB') + imNumComponents = 3 + imBitsPerComponent = 8 + + imwidth, imheight = myimage.size + # print 'imwidth = %s, imheight = %s' % myimage.size + if not x2: + x2 = imwidth + x1 + if not y2: + y2 = y1 + imheight + drawwidth = x2 - x1 + drawheight = y2 - y1 + self.code.extend([ + 'gsave', + '%s %s translate' % (x1,-y1 - drawheight), # need to start are lower left of image + '%s %s scale' % (drawwidth,drawheight)]) + + if imNumComponents == 3 : + self.code.append('/DeviceRGB setcolorspace') + elif imNumComponents == 1 : + self.code.append('/DeviceGray setcolorspace') + print 'setting colorspace gray' + # create the image dictionary + self.code.append(""" +<< +/ImageType 1 +/Width %d /Height %d %% dimensions of source image +/BitsPerComponent %d""" % (imwidth, imheight, imBitsPerComponent) ) + + if imNumComponents == 1: + self.code.append('/Decode [0 1]') + if imNumComponents == 3: + self.code.append('/Decode [0 1 0 1 0 1] %% decode color values normally') + + self.code.extend([ '/ImageMatrix [%s 0 0 %s 0 %s]' % (imwidth, -imheight, imheight), + '/DataSource currentfile /ASCIIHexDecode filter', + '>> % End image dictionary', + 'image']) + # after image operator just need to dump image dat to file as hexstring + rawimage = myimage.tostring() + assert(len(rawimage) == imwidth*imheight, 'Wrong amount of data for image') + #compressed = zlib.compress(rawimage) # no zlib at moment + hex_encoded = self._AsciiHexEncode(rawimage) + + # write in blocks of 78 chars per line + outstream = getStringIO(hex_encoded) + + dataline = outstream.read(78) + while dataline <> "": + self.code.append(dataline) + dataline= outstream.read(78) + self.code.append('> % end of image data') # > is EOD for hex encoded filterfor clarity + self.code.append('grestore') # return coordinates to normal + +# renderpdf - draws them onto a canvas +"""Usage: + from reportlab.graphics import renderPS + renderPS.draw(drawing, canvas, x, y) +Execute the script to see some test drawings.""" +from shapes import * +from renderbase import Renderer + +# hack so we only get warnings once each +#warnOnce = WarnOnce() + +# the main entry point for users... +def draw(drawing, canvas, x=0, y=0, showBoundary=rl_config.showBoundary): + """As it says""" + R = _PSRenderer() + R.draw(drawing, canvas, x, y, showBoundary=showBoundary) + +def _pointsFromList(L): + ''' + given a list of coordinates [x0, y0, x1, y1....] + produce a list of points [(x0,y0), (y1,y0),....] + ''' + P=[] + for i in range(0,len(L),2): + P.append((L[i],L[i+1])) + return P + +class _PSRenderer(Renderer): + """This draws onto a EPS document. It needs to be a class + rather than a function, as some EPS-specific state tracking is + needed outside of the state info in the SVG model.""" + + def __init__(self): + self._tracker = StateTracker() + + def drawNode(self, node): + """This is the recursive method called for each node + in the tree""" + self._canvas.comment('begin node %s'%`node`) + color = self._canvas._color + if not (isinstance(node, Path) and node.isClipPath): + self._canvas.saveState() + + #apply state changes + deltas = getStateDelta(node) + self._tracker.push(deltas) + self.applyStateChanges(deltas, {}) + + #draw the object, or recurse + self.drawNodeDispatcher(node) + + rDeltas = self._tracker.pop() + if not (isinstance(node, Path) and node.isClipPath): + self._canvas.restoreState() + self._canvas.comment('end node %s'%`node`) + self._canvas._color = color + + #restore things we might have lost (without actually doing anything). + for k, v in rDeltas.items(): + if self._restores.has_key(k): + setattr(self._canvas,self._restores[k],v) + +## _restores = {'stroke':'_stroke','stroke_width': '_lineWidth','stroke_linecap':'_lineCap', +## 'stroke_linejoin':'_lineJoin','fill':'_fill','font_family':'_font', +## 'font_size':'_fontSize'} + _restores = {'strokeColor':'_strokeColor','strokeWidth': '_lineWidth','strokeLineCap':'_lineCap', + 'strokeLineJoin':'_lineJoin','fillColor':'_fillColor','fontName':'_font', + 'fontSize':'_fontSize'} + + def drawRect(self, rect): + if rect.rx == rect.ry == 0: + #plain old rectangle + self._canvas.rect( + rect.x, rect.y, + rect.x+rect.width, rect.y+rect.height) + else: + #cheat and assume ry = rx; better to generalize + #pdfgen roundRect function. TODO + self._canvas.roundRect( + rect.x, rect.y, + rect.x+rect.width, rect.y+rect.height, rect.rx, rect.ry + ) + + def drawLine(self, line): + if self._canvas._strokeColor: + self._canvas.line(line.x1, line.y1, line.x2, line.y2) + + def drawCircle(self, circle): + self._canvas.circle( circle.cx, circle.cy, circle.r) + + def drawWedge(self, wedge): + yradius, radius1, yradius1 = wedge._xtraRadii() + if (radius1==0 or radius1 is None) and (yradius1==0 or yradius1 is None): + startangledegrees = wedge.startangledegrees + endangledegrees = wedge.endangledegrees + centerx= wedge.centerx + centery = wedge.centery + radius = wedge.radius + extent = endangledegrees - startangledegrees + self._canvas.drawArc(centerx-radius, centery-yradius, centerx+radius, centery+yradius, + startangledegrees, extent, fromcenter=1) + else: + self.drawPolygon(wedge.asPolygon()) + + def drawPolyLine(self, p): + if self._canvas._strokeColor: + self._canvas.polyLine(_pointsFromList(p.points)) + + def drawEllipse(self, ellipse): + #need to convert to pdfgen's bounding box representation + x1 = ellipse.cx - ellipse.rx + x2 = ellipse.cx + ellipse.rx + y1 = ellipse.cy - ellipse.ry + y2 = ellipse.cy + ellipse.ry + self._canvas.ellipse(x1,y1,x2,y2) + + def drawPolygon(self, p): + self._canvas.polygon(_pointsFromList(p.points), closed=1) + + def drawString(self, stringObj): + if self._canvas._fillColor: + S = self._tracker.getState() + text_anchor, x, y, text = S['textAnchor'], stringObj.x,stringObj.y,stringObj.text + if not text_anchor in ['start','inherited']: + font, fontSize = S['fontName'], S['fontSize'] + textLen = stringWidth(text, font,fontSize) + if text_anchor=='end': + x = x-textLen + elif text_anchor=='middle': + x = x - textLen/2 + else: + raise ValueError, 'bad value for text_anchor '+str(text_anchor) + self._canvas.drawString(x,y,text) + + def drawPath(self, path): + from reportlab.graphics.shapes import _renderPath + c = self._canvas + drawFuncs = (c.moveTo, c.lineTo, c.curveTo, c.closePath) + isClosed = _renderPath(path, drawFuncs) + if not isClosed: + c._fillColor = None + c._fillAndStroke([], clip=path.isClipPath) + + def applyStateChanges(self, delta, newState): + """This takes a set of states, and outputs the operators + needed to set those properties""" + for key, value in delta.items(): + if key == 'transform': + self._canvas.transform(value[0], value[1], value[2], + value[3], value[4], value[5]) + elif key == 'strokeColor': + #this has different semantics in PDF to SVG; + #we always have a color, and either do or do + #not apply it; in SVG one can have a 'None' color + self._canvas.setStrokeColor(value) + elif key == 'strokeWidth': + self._canvas.setLineWidth(value) + elif key == 'strokeLineCap': #0,1,2 + self._canvas.setLineCap(value) + elif key == 'strokeLineJoin': + self._canvas.setLineJoin(value) + elif key == 'strokeDashArray': + if value: + self._canvas.setDash(value) + else: + self._canvas.setDash() +## elif key == 'stroke_opacity': +## warnOnce('Stroke Opacity not supported yet') + elif key == 'fillColor': + #this has different semantics in PDF to SVG; + #we always have a color, and either do or do + #not apply it; in SVG one can have a 'None' color + self._canvas.setFillColor(value) +## elif key == 'fill_rule': +## warnOnce('Fill rules not done yet') +## elif key == 'fill_opacity': +## warnOnce('Fill opacity not done yet') + elif key in ['fontSize', 'fontName']: + # both need setting together in PDF + # one or both might be in the deltas, + # so need to get whichever is missing + fontname = delta.get('fontName', self._canvas._font) + fontsize = delta.get('fontSize', self._canvas._fontSize) + self._canvas.setFont(fontname, fontsize) + +def drawToFile(d,fn, showBoundary=rl_config.showBoundary): + c = PSCanvas((d.width,d.height)) + draw(d, c, 0, 0, showBoundary=showBoundary) + c.save(fn) + +def drawToString(d, showBoundary=rl_config.showBoundary): + "Returns a PS as a string in memory, without touching the disk" + s = getStringIO() + drawToFile(d, s, showBoundary=showBoundary) + return s.getvalue() + +######################################################### +# +# test code. First, defin a bunch of drawings. +# Routine to draw them comes at the end. +# +######################################################### +def test(outdir='epsout'): + import os + # print all drawings and their doc strings from the test + # file + if not os.path.isdir(outdir): + os.mkdir(outdir) + #grab all drawings from the test module + import testshapes + drawings = [] + + for funcname in dir(testshapes): + #if funcname[0:11] == 'getDrawing2': + # print 'hacked to only show drawing 2' + if funcname[0:10] == 'getDrawing': + drawing = eval('testshapes.' + funcname + '()') #execute it + docstring = eval('testshapes.' + funcname + '.__doc__') + drawings.append((drawing, docstring)) + + i = 0 + for (d, docstring) in drawings: + filename = outdir + os.sep + 'renderPS_%d.eps'%i + drawToFile(d,filename) + print 'saved', filename + i = i + 1 + +if __name__=='__main__': + import sys + if len(sys.argv)>1: + outdir = sys.argv[1] + else: + outdir = 'epsout' + test(outdir) diff --git a/bin/reportlab/graphics/renderSVG.py b/bin/reportlab/graphics/renderSVG.py new file mode 100644 index 00000000000..c987dc8a695 --- /dev/null +++ b/bin/reportlab/graphics/renderSVG.py @@ -0,0 +1,813 @@ +"""An experimental SVG renderer for the ReportLab graphics framework. + +This will create SVG code from the ReportLab Graphics API (RLG). +To read existing SVG code and convert it into ReportLab graphics +objects download the svglib module here: + + http://python.net/~gherman/#svglib +""" + +import math, string, types, sys, os +from types import StringType +from operator import getitem + +from reportlab.pdfbase.pdfmetrics import stringWidth # for font info +from reportlab.lib.utils import fp_str +from reportlab.lib.colors import black +from reportlab.graphics.renderbase import StateTracker, getStateDelta, Renderer +from reportlab.graphics.shapes import STATE_DEFAULTS, Path, UserNode +from reportlab.graphics.shapes import * # (only for test0) +from reportlab import rl_config +from reportlab.lib.utils import getStringIO + +from xml.dom import getDOMImplementation + + +### some constants ### + +sin = math.sin +cos = math.cos +pi = math.pi + +LINE_STYLES = 'stroke-width stroke-linecap stroke fill stroke-dasharray' +TEXT_STYLES = 'font-family font-size' + + +### top-level user function ### + +def drawToString(d, showBoundary=rl_config.showBoundary): + "Returns a SVG as a string in memory, without touching the disk" + s = getStringIO() + drawToFile(d, s, showBoundary=showBoundary) + return s.getvalue() + + +def drawToFile(d, fn, showBoundary=rl_config.showBoundary): + c = SVGCanvas((d.width, d.height)) + draw(d, c, 0, 0, showBoundary=showBoundary) + c.save(fn) + + +def draw(drawing, canvas, x=0, y=0, showBoundary=rl_config.showBoundary): + """As it says.""" + + r = _SVGRenderer() + r.draw(drawing, canvas, x, y, showBoundary=showBoundary) + + +### helper functions ### + +def _pointsFromList(L): + """ + given a list of coordinates [x0, y0, x1, y1....] + produce a list of points [(x0,y0), (y1,y0),....] + """ + + P=[] + for i in range(0,len(L), 2): + P.append((L[i], L[i+1])) + + return P + + +def transformNode(doc, newTag, node=None, **attrDict): + """Transform a DOM node into new node and copy selected attributes. + + Creates a new DOM node with tag name 'newTag' for document 'doc' + and copies selected attributes from an existing 'node' as provided + in 'attrDict'. The source 'node' can be None. Attribute values will + be converted to strings. + + E.g. + + n = transformNode(doc, "node1", x="0", y="1") + -> DOM node for <node1 x="0" y="1"/> + + n = transformNode(doc, "node1", x=0, y=1+1) + -> DOM node for <node1 x="0" y="2"/> + + n = transformNode(doc, "node1", node0, x="x0", y="x0", zoo=bar()) + -> DOM node for <node1 x="[node0.x0]" y="[node0.y0]" zoo="[bar()]"/> + """ + + newNode = doc.createElement(newTag) + for newAttr, attr in attrDict.items(): + sattr = str(attr) + if not node: + newNode.setAttribute(newAttr, sattr) + else: + attrVal = node.getAttribute(sattr) + newNode.setAttribute(newAttr, attrVal or sattr) + + return newNode + + +### classes ### + +class SVGCanvas: + def __init__(self, size=(300,300)): + self.verbose = 0 + self.width, self.height = self.size = size + # self.height = size[1] + self.code = [] + self.style = {} + self.path = '' + self._strokeColor = self._fillColor = self._lineWidth = \ + self._font = self._fontSize = self._lineCap = \ + self._lineJoin = self._color = None + + implementation = getDOMImplementation('minidom') + self.doc = implementation.createDocument(None, "svg", None) + self.svg = self.doc.documentElement + self.svg.setAttribute("width", str(size[0])) + self.svg.setAttribute("height", str(self.height)) + + title = self.doc.createElement('title') + text = self.doc.createTextNode('...') + title.appendChild(text) + self.svg.appendChild(title) + + desc = self.doc.createElement('desc') + text = self.doc.createTextNode('...') + desc.appendChild(text) + self.svg.appendChild(desc) + + self.setFont(STATE_DEFAULTS['fontName'], STATE_DEFAULTS['fontSize']) + self.setStrokeColor(STATE_DEFAULTS['strokeColor']) + self.setLineCap(2) + self.setLineJoin(0) + self.setLineWidth(1) + + # Add a rectangular clipping path identical to view area. + clipPath = transformNode(self.doc, "clipPath", id="clip") + clipRect = transformNode(self.doc, "rect", x=0, y=0, + width=self.width, height=self.height) + clipPath.appendChild(clipRect) + self.svg.appendChild(clipPath) + + self.groupTree = transformNode(self.doc, "g", + id="group", + transform="scale(1,-1) translate(0,-%d)" % self.height, + style="clip-path: url(#clip)") + self.svg.appendChild(self.groupTree) + self.currGroup = self.groupTree + + + def save(self, f=None): + if type(f) is StringType: + file = open(f, 'w') + else: + file = f + + file.write("""\ +<?xml version="1.0" encoding="iso-8859-1"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20000303 Stylable//EN" "http://www.w3.org/TR/2000/03/WD-SVG-20000303/DTD/svg-20000303-stylable.dtd" >\n""") + + # use = self.doc.createElement('use') + # use.setAttribute("xlink:href", "#group") + # use.setAttribute("transform", "scale(1, -1)") + # self.svg.appendChild(use) + + result = self.svg.toprettyxml(indent=" ") + file.write(result) + + if file is not f: + file.close() + + + ### helpers ### + + def NOTUSED_stringWidth(self, s, font=None, fontSize=None): + """Return the logical width of the string if it were drawn + in the current font (defaults to self.font). + """ + + font = font or self._font + fontSize = fontSize or self._fontSize + + return stringWidth(s, font, fontSize) + + + def _formatStyle(self, include=''): + str = '' + include = string.split(include) + keys = self.style.keys() + if include: + #2.1-safe version of the line below follows: + #keys = filter(lambda k: k in include, keys) + tmp = [] + for word in keys: + if word in include: + tmp.append(word) + keys = tmp + + items = [] + for k in keys: + items.append((k, self.style[k])) + items = map(lambda i: "%s: %s"%(i[0], i[1]), items) + str = string.join(items, '; ') + ';' + + return str + + + def _escape(self, s): + """ + return a copy of string s with special characters in postscript strings + escaped with backslashes. + Have not handled characters that are converted normally in python strings + i.e. \n -> newline + """ + + str = string.replace(s, chr(0x5C), r'\\' ) + str = string.replace(str, '(', '\(' ) + str = string.replace(str, ')', '\)') + return str + + + def _genArcCode(self, x1, y1, x2, y2, startAng, extent): + """Calculate the path for an arc inscribed in rectangle defined + by (x1,y1),(x2,y2).""" + + return + + #calculate semi-minor and semi-major axes of ellipse + xScale = abs((x2-x1)/2.0) + yScale = abs((y2-y1)/2.0) + #calculate centre of ellipse + x, y = (x1+x2)/2.0, (y1+y2)/2.0 + + codeline = 'matrix currentmatrix %s %s translate %s %s scale 0 0 1 %s %s %s setmatrix' + + if extent >= 0: + arc='arc' + else: + arc='arcn' + data = (x,y, xScale, yScale, startAng, startAng+extent, arc) + + return codeline % data + + + def _fillAndStroke(self, code, clip=0): + path = transformNode(self.doc, "path", + d=self.path, style=self._formatStyle(LINE_STYLES)) + self.currGroup.appendChild(path) + self.path = '' + + return + + """ + if self._fillColor or self._strokeColor or clip: + self.code.extend(code) + if self._fillColor: + if self._strokeColor or clip: + self.code.append("gsave") + self.setColor(self._fillColor) + self.code.append("eofill") + if self._strokeColor or clip: + self.code.append("grestore") + if self._strokeColor != None: + if clip: self.code.append("gsave") + self.setColor(self._strokeColor) + self.code.append("stroke") + if clip: self.code.append("grestore") + if clip: + self.code.append("clip") + self.code.append("newpath") + """ + + + ### styles ### + + def setLineCap(self, v): + vals = {0:'butt', 1:'round', 2:'square'} + if self._lineCap != v: + self._lineCap = v + self.style['stroke-linecap'] = vals[v] + + + def setLineJoin(self, v): + vals = {0:'miter', 1:'round', 2:'bevel'} + if self._lineJoin != v: + self._lineJoin = v + self.style['stroke-linecap'] = vals[v] + + + def setDash(self, array=[], phase=0): + """Two notations. Pass two numbers, or an array and phase.""" + + join = string.join + if type(array) in (types.IntType, types.FloatType): + self.style['stroke-dasharray'] = join(map(str, ([array, phase])), ', ') + elif type(array) in (types.ListType, types.TupleType) and len(array) > 0: + assert phase >= 0, "phase is a length in user space" + self.style['stroke-dasharray'] = join(map(str, (array+[phase])), ', ') + + + def setStrokeColor(self, color): + self._strokeColor = color + self.setColor(color) + if color == None: + self.style['stroke'] = 'none' + else: + r, g, b = color.red, color.green, color.blue + self.style['stroke'] = 'rgb(%d%%,%d%%,%d%%)' % (r*100, g*100, b*100) + + + def setColor(self, color): + if self._color != color: + self._color = color + + + def setFillColor(self, color): + self._fillColor = color + self.setColor(color) + if color == None: + self.style['fill'] = 'none' + else: + r, g, b = color.red, color.green, color.blue + self.style['fill'] = 'rgb(%d%%,%d%%,%d%%)' % (r*100, g*100, b*100) + + + def setLineWidth(self, width): + if width != self._lineWidth: + self._lineWidth = width + self.style['stroke-width'] = width + + + def setFont(self, font, fontSize): + if self._font != font or self._fontSize != fontSize: + self._font, self._fontSize = (font, fontSize) + self.style['font-family'] = font + self.style['font-size'] = fontSize + + + ### shapes ### + + def rect(self, x1,y1, x2,y2, rx=8, ry=8): + "Draw a rectangle between x1,y1 and x2,y2." + + if self.verbose: print "+++ SVGCanvas.rect" + + rect = transformNode(self.doc, "rect", + x=x1, y=y1, width=x2-x1, height=y2-y1, + style=self._formatStyle(LINE_STYLES)) + + self.currGroup.appendChild(rect) + + + def roundRect(self, x1,y1, x2,y2, rx=8, ry=8): + """Draw a rounded rectangle between x1,y1 and x2,y2. + + Corners inset as ellipses with x-radius rx and y-radius ry. + These should have x1<x2, y1<y2, rx>0, and ry>0. + """ + + rect = transformNode(self.doc, "rect", + x=x1, y=y1, width=x2-x1, height=y2-y1, rx=rx, ry=ry, + style=self._formatStyle(LINE_STYLES)) + + self.currGroup.appendChild(rect) + + + def drawString(self, s, x, y, angle=0): + if self.verbose: print "+++ SVGCanvas.drawString" + + if self._fillColor != None: + self.setColor(self._fillColor) + s = self._escape(s) + st = self._formatStyle(TEXT_STYLES) + if angle != 0: + st = st + " rotate(%f %f %f);" % (angle, x, y) + st = st + " fill: %s;" % self.style['fill'] + text = transformNode(self.doc, "text", + x=x, y=y, style=st, + transform="translate(0,%d) scale(1,-1)" % (2*y)) + content = self.doc.createTextNode(s) + text.appendChild(content) + + self.currGroup.appendChild(text) + + + def comment(self, data): + "Add a comment." + + comment = self.doc.createComment(data) + # self.currGroup.appendChild(comment) + + + def drawImage(self, image, x1, y1, x2=None, y2=None): + pass + + + def line(self, x1, y1, x2, y2): + if self._strokeColor != None: + if 0: # something is wrong with line in my SVG viewer... + line = transformNode(self.doc, "line", + x=x1, y=y1, x2=x2, y2=y2, + style=self._formatStyle(LINE_STYLES)) + self.currGroup.appendChild(line) + path = transformNode(self.doc, "path", + d="M %f,%f L %f,%f Z" % (x1,y1,x2,y2), + style=self._formatStyle(LINE_STYLES)) + self.currGroup.appendChild(path) + + + def ellipse(self, x1, y1, x2, y2): + """Draw an orthogonal ellipse inscribed within the rectangle x1,y1,x2,y2. + + These should have x1<x2 and y1<y2. + """ + + ellipse = transformNode(self.doc, "ellipse", + cx=(x1+x2)/2.0, cy=(y1+y2)/2.0, rx=(x2-x1)/2.0, ry=(y2-y1)/2.0, + style=self._formatStyle(LINE_STYLES)) + + self.currGroup.appendChild(ellipse) + + + def circle(self, xc, yc, r): + circle = transformNode(self.doc, "circle", + cx=xc, cy=yc, r=r, + style=self._formatStyle(LINE_STYLES)) + + self.currGroup.appendChild(circle) + + + def drawCurve(self, x1, y1, x2, y2, x3, y3, x4, y4, closed=0): + pass + return + + codeline = '%s m %s curveto' + data = (fp_str(x1, y1), fp_str(x2, y2, x3, y3, x4, y4)) + if self._fillColor != None: + self.setColor(self._fillColor) + self.code.append((codeline % data) + ' eofill') + if self._strokeColor != None: + self.setColor(self._strokeColor) + self.code.append((codeline % data) + + ((closed and ' closepath') or '') + + ' stroke') + + + def drawArc(self, x1,y1, x2,y2, startAng=0, extent=360, fromcenter=0): + """Draw a partial ellipse inscribed within the rectangle x1,y1,x2,y2. + + Starting at startAng degrees and covering extent degrees. Angles + start with 0 to the right (+x) and increase counter-clockwise. + These should have x1<x2 and y1<y2. + """ + + cx, cy = (x1+x2)/2.0, (y1+y2)/2.0 + rx, ry = (x2-x1)/2.0, (y2-y1)/2.0 + mx = rx * cos(startAng*pi/180) + cx + my = ry * sin(startAng*pi/180) + cy + ax = rx * cos((startAng+extent)*pi/180) + cx + ay = ry * sin((startAng+extent)*pi/180) + cy + + str = '' + if fromcenter: + str = str + "M %f, %f L %f, %f " % (cx, cy, ax, ay) + + if fromcenter: + str = str + "A %f, %f %d %d %d %f, %f " % \ + (rx, ry, 0, extent>=180, 0, mx, my) + else: + str = str + "M %f, %f A %f, %f %d %d %d %f, %f Z " % \ + (mx, my, rx, ry, 0, extent>=180, 0, mx, my) + + if fromcenter: + str = str + "L %f, %f Z " % (cx, cy) + + path = transformNode(self.doc, "path", + d=str, style=self._formatStyle()) + self.currGroup.appendChild(path) + + + def polygon(self, points, closed=0): + assert len(points) >= 2, 'Polygon must have 2 or more points' + + if self._strokeColor != None: + self.setColor(self._strokeColor) + pairs = [] + for i in xrange(len(points)): + pairs.append("%f %f" % (points[i])) + pts = string.join(pairs, ', ') + polyline = transformNode(self.doc, "polygon", + points=pts, style=self._formatStyle(LINE_STYLES)) + self.currGroup.appendChild(polyline) + + # self._fillAndStroke(polyCode) + + + def lines(self, lineList, color=None, width=None): + # print "### lineList", lineList + return + + if self._strokeColor != None: + self._setColor(self._strokeColor) + codeline = '%s m %s l stroke' + for line in lineList: + self.code.append(codeline % (fp_str(line[0]), fp_str(line[1]))) + + + def polyLine(self, points): + assert len(points) >= 1, 'Polyline must have 1 or more points' + + if self._strokeColor != None: + self.setColor(self._strokeColor) + pairs = [] + for i in xrange(len(points)): + pairs.append("%f %f" % (points[i])) + pts = string.join(pairs, ', ') + polyline = transformNode(self.doc, "polyline", + points=pts, style=self._formatStyle(LINE_STYLES)) + self.currGroup.appendChild(polyline) + + + ### groups ### + + def startGroup(self): + if self.verbose: print "+++ begin SVGCanvas.startGroup" + currGroup, group = self.currGroup, transformNode(self.doc, "g", transform="") + currGroup.appendChild(group) + self.currGroup = group + if self.verbose: print "+++ end SVGCanvas.startGroup" + return currGroup + + def endGroup(self,currGroup): + if self.verbose: print "+++ begin SVGCanvas.endGroup" + self.currGroup = currGroup + if self.verbose: print "+++ end SVGCanvas.endGroup" + + + def transform(self, a, b, c, d, e, f): + if self.verbose: print "!!! begin SVGCanvas.transform", a, b, c, d, e, f + tr = self.currGroup.getAttribute("transform") + t = 'matrix(%f, %f, %f, %f, %f, %f)' % (a,b,c,d,e,f) + if (a, b, c, d, e, f) != (1, 0, 0, 1, 0, 0): + self.currGroup.setAttribute("transform", "%s %s" % (tr, t)) + + + def translate(self, x, y): + # probably never used + print "!!! begin SVGCanvas.translate" + return + + tr = self.currGroup.getAttribute("transform") + t = 'translate(%f, %f)' % (x, y) + self.currGroup.setAttribute("transform", "%s %s" % (tr, t)) + + + def scale(self, x, y): + # probably never used + print "!!! begin SVGCanvas.scale" + return + + tr = self.groups[-1].getAttribute("transform") + t = 'scale(%f, %f)' % (x, y) + self.currGroup.setAttribute("transform", "%s %s" % (tr, t)) + + + ### paths ### + + def moveTo(self, x, y): + self.path = self.path + 'M %f %f ' % (x, y) + + + def lineTo(self, x, y): + self.path = self.path + 'L %f %f ' % (x, y) + + + def curveTo(self, x1, y1, x2, y2, x3, y3): + self.path = self.path + 'C %f %f %f %f %f %f ' % (x1, y1, x2, y2, x3, y3) + + + def closePath(self): + self.path = self.path + 'Z ' + + def saveState(self): + pass + + def restoreState(self): + pass + +class _SVGRenderer(Renderer): + """This draws onto an SVG document. + """ + + def __init__(self): + self._tracker = StateTracker() + self.verbose = 0 + + def drawNode(self, node): + """This is the recursive method called for each node in the tree. + """ + + if self.verbose: print "### begin _SVGRenderer.drawNode" + + self._canvas.comment('begin node %s'%`node`) + color = self._canvas._color + if not (isinstance(node, Path) and node.isClipPath): + pass # self._canvas.saveState() + + #apply state changes + deltas = getStateDelta(node) + self._tracker.push(deltas) + self.applyStateChanges(deltas, {}) + + #draw the object, or recurse + self.drawNodeDispatcher(node) + + rDeltas = self._tracker.pop() + if not (isinstance(node, Path) and node.isClipPath): + pass # self._canvas.restoreState() + self._canvas.comment('end node %s'%`node`) + self._canvas._color = color + + #restore things we might have lost (without actually doing anything). + for k, v in rDeltas.items(): + if self._restores.has_key(k): + setattr(self._canvas,self._restores[k],v) + + if self.verbose: print "### end _SVGRenderer.drawNode" + + _restores = {'strokeColor':'_strokeColor','strokeWidth': '_lineWidth','strokeLineCap':'_lineCap', + 'strokeLineJoin':'_lineJoin','fillColor':'_fillColor','fontName':'_font', + 'fontSize':'_fontSize'} + + + def drawGroup(self, group): + if self.verbose: print "### begin _SVGRenderer.drawGroup" + + currGroup = self._canvas.startGroup() + a, b, c, d, e, f = self._tracker.getCTM() + for childNode in group.getContents(): + if isinstance(childNode, UserNode): + node2 = childNode.provideNode() + else: + node2 = childNode + self.drawNode(node2) + self._canvas.transform(a, b, c, d, e, f) + self._canvas.endGroup(currGroup) + + if self.verbose: print "### end _SVGRenderer.drawGroup" + + + def drawRect(self, rect): + if rect.rx == rect.ry == 0: + #plain old rectangle + self._canvas.rect( + rect.x, rect.y, + rect.x+rect.width, rect.y+rect.height) + else: + #cheat and assume ry = rx; better to generalize + #pdfgen roundRect function. TODO + self._canvas.roundRect( + rect.x, rect.y, + rect.x+rect.width, rect.y+rect.height, + rect.rx, rect.ry + ) + + + def drawString(self, stringObj): + if self._canvas._fillColor: + S = self._tracker.getState() + text_anchor, x, y, text = S['textAnchor'], stringObj.x, stringObj.y, stringObj.text + if not text_anchor in ['start', 'inherited']: + font, fontSize = S['fontName'], S['fontSize'] + textLen = stringWidth(text, font,fontSize) + if text_anchor=='end': + x = x-textLen + elif text_anchor=='middle': + x = x - textLen/2 + else: + raise ValueError, 'bad value for text_anchor ' + str(text_anchor) + self._canvas.drawString(text,x,y) + + + def drawLine(self, line): + if self._canvas._strokeColor: + self._canvas.line(line.x1, line.y1, line.x2, line.y2) + + + def drawCircle(self, circle): + self._canvas.circle( circle.cx, circle.cy, circle.r) + + + def drawWedge(self, wedge): + centerx, centery, radius, startangledegrees, endangledegrees = \ + wedge.centerx, wedge.centery, wedge.radius, wedge.startangledegrees, wedge.endangledegrees + yradius = wedge.yradius or wedge.radius + (x1, y1) = (centerx-radius, centery-yradius) + (x2, y2) = (centerx+radius, centery+yradius) + extent = endangledegrees - startangledegrees + self._canvas.drawArc(x1, y1, x2, y2, startangledegrees, extent, fromcenter=1) + + + def drawPolyLine(self, p): + if self._canvas._strokeColor: + self._canvas.polyLine(_pointsFromList(p.points)) + + + def drawEllipse(self, ellipse): + #need to convert to pdfgen's bounding box representation + x1 = ellipse.cx - ellipse.rx + x2 = ellipse.cx + ellipse.rx + y1 = ellipse.cy - ellipse.ry + y2 = ellipse.cy + ellipse.ry + self._canvas.ellipse(x1,y1,x2,y2) + + + def drawPolygon(self, p): + self._canvas.polygon(_pointsFromList(p.points), closed=1) + + + def drawPath(self, path): + # print "### drawPath", path.points + from reportlab.graphics.shapes import _renderPath + c = self._canvas + drawFuncs = (c.moveTo, c.lineTo, c.curveTo, c.closePath) + isClosed = _renderPath(path, drawFuncs) + if not isClosed: + c._fillColor = None + c._fillAndStroke([], clip=path.isClipPath) + + + def applyStateChanges(self, delta, newState): + """This takes a set of states, and outputs the operators + needed to set those properties""" + + for key, value in delta.items(): + if key == 'transform': + pass + #self._canvas.transform(value[0], value[1], value[2], value[3], value[4], value[5]) + elif key == 'strokeColor': + self._canvas.setStrokeColor(value) + elif key == 'strokeWidth': + self._canvas.setLineWidth(value) + elif key == 'strokeLineCap': #0,1,2 + self._canvas.setLineCap(value) + elif key == 'strokeLineJoin': + self._canvas.setLineJoin(value) + elif key == 'strokeDashArray': + if value: + self._canvas.setDash(value) + else: + self._canvas.setDash() + elif key == 'fillColor': + self._canvas.setFillColor(value) + elif key in ['fontSize', 'fontName']: + fontname = delta.get('fontName', self._canvas._font) + fontsize = delta.get('fontSize', self._canvas._fontSize) + self._canvas.setFont(fontname, fontsize) + + + + +def test0(outdir='svgout'): + # print all drawings and their doc strings from the test + # file + if not os.path.isdir(outdir): + os.mkdir(outdir) + #grab all drawings from the test module + from reportlab.graphics import testshapes + drawings = [] + + for funcname in dir(testshapes): + #if funcname[0:11] == 'getDrawing2': + # print 'hacked to only show drawing 2' + if funcname[0:10] == 'getDrawing': + drawing = eval('testshapes.' + funcname + '()') + docstring = eval('testshapes.' + funcname + '.__doc__') + drawings.append((drawing, docstring)) + + # return + + i = 0 + for (d, docstring) in drawings: + filename = outdir + os.sep + 'renderSVG_%d.svg' % i + drawToFile(d, filename) + # print 'saved', filename + i = i + 1 + + +def test1(): + from reportlab.graphics.testshapes import getDrawing01 + d = getDrawing01() + drawToFile(d, "svgout/test.svg") + + +def test2(): + from reportlab.lib.corp import RL_CorpLogo + from reportlab.graphics.shapes import Drawing + + rl = RL_CorpLogo() + d = Drawing(rl.width,rl.height) + d.add(rl) + drawToFile(d, "svgout/corplogo.svg") + + +if __name__=='__main__': + test0() + test1() + test2() diff --git a/bin/reportlab/graphics/renderbase.py b/bin/reportlab/graphics/renderbase.py new file mode 100644 index 00000000000..51f73f4d2a5 --- /dev/null +++ b/bin/reportlab/graphics/renderbase.py @@ -0,0 +1,315 @@ +#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/renderbase.py +""" +Superclass for renderers to factor out common functionality and default implementations. +""" + + +__version__=''' $Id $ ''' + +from reportlab.graphics.shapes import * +from reportlab import rl_config + +def inverse(A): + "For A affine 2D represented as 6vec return 6vec version of A**(-1)" + # I checked this RGB + det = float(A[0]*A[3] - A[2]*A[1]) + R = [A[3]/det, -A[1]/det, -A[2]/det, A[0]/det] + return tuple(R+[-R[0]*A[4]-R[2]*A[5],-R[1]*A[4]-R[3]*A[5]]) + +def mmult(A, B): + "A postmultiplied by B" + # I checked this RGB + # [a0 a2 a4] [b0 b2 b4] + # [a1 a3 a5] * [b1 b3 b5] + # [ 1 ] [ 1 ] + # + return (A[0]*B[0] + A[2]*B[1], + A[1]*B[0] + A[3]*B[1], + A[0]*B[2] + A[2]*B[3], + A[1]*B[2] + A[3]*B[3], + A[0]*B[4] + A[2]*B[5] + A[4], + A[1]*B[4] + A[3]*B[5] + A[5]) + + +def getStateDelta(shape): + """Used to compute when we need to change the graphics state. + For example, if we have two adjacent red shapes we don't need + to set the pen color to red in between. Returns the effect + the given shape would have on the graphics state""" + delta = {} + for (prop, value) in shape.getProperties().items(): + if STATE_DEFAULTS.has_key(prop): + delta[prop] = value + return delta + + +class StateTracker: + """Keeps a stack of transforms and state + properties. It can contain any properties you + want, but the keys 'transform' and 'ctm' have + special meanings. The getCTM() + method returns the current transformation + matrix at any point, without needing to + invert matrixes when you pop.""" + def __init__(self, defaults=None): + # one stack to keep track of what changes... + self.__deltas = [] + + # and another to keep track of cumulative effects. Last one in + # list is the current graphics state. We put one in to simplify + # loops below. + self.__combined = [] + if defaults is None: + defaults = STATE_DEFAULTS.copy() + #ensure that if we have a transform, we have a CTM + if defaults.has_key('transform'): + defaults['ctm'] = defaults['transform'] + self.__combined.append(defaults) + + def push(self,delta): + """Take a new state dictionary of changes and push it onto + the stack. After doing this, the combined state is accessible + through getState()""" + + newstate = self.__combined[-1].copy() + for (key, value) in delta.items(): + if key == 'transform': #do cumulative matrix + newstate['transform'] = delta['transform'] + newstate['ctm'] = mmult(self.__combined[-1]['ctm'], delta['transform']) + #print 'statetracker transform = (%0.2f, %0.2f, %0.2f, %0.2f, %0.2f, %0.2f)' % tuple(newstate['transform']) + #print 'statetracker ctm = (%0.2f, %0.2f, %0.2f, %0.2f, %0.2f, %0.2f)' % tuple(newstate['ctm']) + + else: #just overwrite it + newstate[key] = value + + self.__combined.append(newstate) + self.__deltas.append(delta) + + def pop(self): + """steps back one, and returns a state dictionary with the + deltas to reverse out of wherever you are. Depending + on your back end, you may not need the return value, + since you can get the complete state afterwards with getState()""" + del self.__combined[-1] + newState = self.__combined[-1] + lastDelta = self.__deltas[-1] + del self.__deltas[-1] + #need to diff this against the last one in the state + reverseDelta = {} + #print 'pop()...' + for key, curValue in lastDelta.items(): + #print ' key=%s, value=%s' % (key, curValue) + prevValue = newState[key] + if prevValue <> curValue: + #print ' state popping "%s"="%s"' % (key, curValue) + if key == 'transform': + reverseDelta[key] = inverse(lastDelta['transform']) + else: #just return to previous state + reverseDelta[key] = prevValue + return reverseDelta + + def getState(self): + "returns the complete graphics state at this point" + return self.__combined[-1] + + def getCTM(self): + "returns the current transformation matrix at this point""" + return self.__combined[-1]['ctm'] + + def __getitem__(self,key): + "returns the complete graphics state value of key at this point" + return self.__combined[-1][key] + + def __setitem__(self,key,value): + "sets the complete graphics state value of key to value" + self.__combined[-1][key] = value + +def testStateTracker(): + print 'Testing state tracker' + defaults = {'fillColor':None, 'strokeColor':None,'fontName':None, 'transform':[1,0,0,1,0,0]} + deltas = [ + {'fillColor':'red'}, + {'fillColor':'green', 'strokeColor':'blue','fontName':'Times-Roman'}, + {'transform':[0.5,0,0,0.5,0,0]}, + {'transform':[0.5,0,0,0.5,2,3]}, + {'strokeColor':'red'} + ] + + st = StateTracker(defaults) + print 'initial:', st.getState() + print + for delta in deltas: + print 'pushing:', delta + st.push(delta) + print 'state: ',st.getState(),'\n' + + for delta in deltas: + print 'popping:',st.pop() + print 'state: ',st.getState(),'\n' + + +def _expandUserNode(node,canvas): + if isinstance(node, UserNode): + try: + if hasattr(node,'_canvas'): + ocanvas = 1 + else: + node._canvas = canvas + ocanvas = None + onode = node + node = node.provideNode() + finally: + if not ocanvas: del onode._canvas + return node + +class Renderer: + """Virtual superclass for graphics renderers.""" + + def __init__(self): + self._tracker = StateTracker() + + def undefined(self, operation): + raise ValueError, "%s operation not defined at superclass class=%s" %(operation, self.__class__) + + def draw(self, drawing, canvas, x=0, y=0, showBoundary=rl_config._unset_): + """This is the top level function, which draws the drawing at the given + location. The recursive part is handled by drawNode.""" + #stash references for ease of communication + if showBoundary is rl_config._unset_: showBoundary=rl_config.showBoundary + self._canvas = canvas + canvas.__dict__['_drawing'] = self._drawing = drawing + drawing._parent = None + try: + #bounding box + if showBoundary: canvas.rect(x, y, drawing.width, drawing.height) + canvas.saveState() + self.initState(x,y) + self.drawNode(drawing) + self.pop() + canvas.restoreState() + finally: + #remove any circular references + del self._canvas, self._drawing, canvas._drawing, drawing._parent + + def initState(self,x,y): + deltas = STATE_DEFAULTS.copy() + deltas['transform'] = [1,0,0,1,x,y] + self._tracker.push(deltas) + self.applyStateChanges(deltas, {}) + + def pop(self): + self._tracker.pop() + + def drawNode(self, node): + """This is the recursive method called for each node + in the tree""" + # Undefined here, but with closer analysis probably can be handled in superclass + self.undefined("drawNode") + + def drawNodeDispatcher(self, node): + """dispatch on the node's (super) class: shared code""" + + canvas = getattr(self,'_canvas',None) + # replace UserNode with its contents + + try: + node = _expandUserNode(node,canvas) + if hasattr(node,'_canvas'): + ocanvas = 1 + else: + node._canvas = canvas + ocanvas = None + + #draw the object, or recurse + if isinstance(node, Line): + self.drawLine(node) + elif isinstance(node, Image): + self.drawImage(node) + elif isinstance(node, Rect): + self.drawRect(node) + elif isinstance(node, Circle): + self.drawCircle(node) + elif isinstance(node, Ellipse): + self.drawEllipse(node) + elif isinstance(node, PolyLine): + self.drawPolyLine(node) + elif isinstance(node, Polygon): + self.drawPolygon(node) + elif isinstance(node, Path): + self.drawPath(node) + elif isinstance(node, String): + self.drawString(node) + elif isinstance(node, Group): + self.drawGroup(node) + elif isinstance(node, Wedge): + self.drawWedge(node) + else: + print 'DrawingError','Unexpected element %s in drawing!' % str(node) + finally: + if not ocanvas: del node._canvas + + _restores = {'stroke':'_stroke','stroke_width': '_lineWidth','stroke_linecap':'_lineCap', + 'stroke_linejoin':'_lineJoin','fill':'_fill','font_family':'_font', + 'font_size':'_fontSize'} + + def drawGroup(self, group): + # just do the contents. Some renderers might need to override this + # if they need a flipped transform + canvas = getattr(self,'_canvas',None) + for node in group.getContents(): + node = _expandUserNode(node,canvas) + try: + if hasattr(node,'_canvas'): + ocanvas = 1 + else: + node._canvas = canvas + ocanvas = None + node._parent = group + self.drawNode(node) + finally: + del node._parent + if not ocanvas: del node._canvas + + def drawWedge(self, wedge): + # by default ask the wedge to make a polygon of itself and draw that! + #print "drawWedge" + polygon = wedge.asPolygon() + self.drawPolygon(polygon) + + def drawPath(self, path): + polygons = path.asPolygons() + for polygon in polygons: + self.drawPolygon(polygon) + + def drawRect(self, rect): + # could be implemented in terms of polygon + self.undefined("drawRect") + + def drawLine(self, line): + self.undefined("drawLine") + + def drawCircle(self, circle): + self.undefined("drawCircle") + + def drawPolyLine(self, p): + self.undefined("drawPolyLine") + + def drawEllipse(self, ellipse): + self.undefined("drawEllipse") + + def drawPolygon(self, p): + self.undefined("drawPolygon") + + def drawString(self, stringObj): + self.undefined("drawString") + + def applyStateChanges(self, delta, newState): + """This takes a set of states, and outputs the operators + needed to set those properties""" + self.undefined("applyStateChanges") + +if __name__=='__main__': + print "this file has no script interpretation" + print __doc__ diff --git a/bin/reportlab/graphics/samples/__init__.py b/bin/reportlab/graphics/samples/__init__.py new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/bin/reportlab/graphics/samples/__init__.py diff --git a/bin/reportlab/graphics/samples/bubble.py b/bin/reportlab/graphics/samples/bubble.py new file mode 100644 index 00000000000..64bf3137912 --- /dev/null +++ b/bin/reportlab/graphics/samples/bubble.py @@ -0,0 +1,73 @@ +#Autogenerated by ReportLab guiedit do not edit +from reportlab.graphics.charts.legends import Legend +from reportlab.graphics.charts.lineplots import ScatterPlot +from reportlab.graphics.shapes import Drawing, _DrawingEditorMixin, String +from reportlab.graphics.charts.textlabels import Label +from excelcolors import * + +class Bubble(_DrawingEditorMixin,Drawing): + def __init__(self,width=200,height=150,*args,**kw): + apply(Drawing.__init__,(self,width,height)+args,kw) + self._add(self,ScatterPlot(),name='chart',validate=None,desc="The main chart") + self.chart.width = 115 + self.chart.height = 80 + self.chart.x = 30 + self.chart.y = 40 + self.chart.lines[0].strokeColor = color01 + self.chart.lines[1].strokeColor = color02 + self.chart.lines[2].strokeColor = color03 + self.chart.lines[3].strokeColor = color04 + self.chart.lines[4].strokeColor = color05 + self.chart.lines[5].strokeColor = color06 + self.chart.lines[6].strokeColor = color07 + self.chart.lines[7].strokeColor = color08 + self.chart.lines[8].strokeColor = color09 + self.chart.lines[9].strokeColor = color10 + self.chart.lines.symbol.kind ='Circle' + self.chart.lines.symbol.size = 15 + self.chart.fillColor = backgroundGrey + self.chart.lineLabels.fontName = 'Helvetica' + self.chart.xValueAxis.labels.fontName = 'Helvetica' + self.chart.xValueAxis.labels.fontSize = 7 + self.chart.xValueAxis.forceZero = 0 + self.chart.data = [((100,100), (200,200), (250,210), (300,300), (350,450))] + self.chart.xValueAxis.avoidBoundFrac = 1 + self.chart.xValueAxis.gridEnd = 115 + self.chart.xValueAxis.tickDown = 3 + self.chart.xValueAxis.visibleGrid = 1 + self.chart.yValueAxis.tickLeft = 3 + self.chart.yValueAxis.labels.fontName = 'Helvetica' + self.chart.yValueAxis.labels.fontSize = 7 + self._add(self,Label(),name='Title',validate=None,desc="The title at the top of the chart") + self.Title.fontName = 'Helvetica-Bold' + self.Title.fontSize = 7 + self.Title.x = 100 + self.Title.y = 135 + self.Title._text = 'Chart Title' + self.Title.maxWidth = 180 + self.Title.height = 20 + self.Title.textAnchor ='middle' + self._add(self,Legend(),name='Legend',validate=None,desc="The legend or key for the chart") + self.Legend.colorNamePairs = [(color01, 'Widgets')] + self.Legend.fontName = 'Helvetica' + self.Legend.fontSize = 7 + self.Legend.x = 153 + self.Legend.y = 85 + self.Legend.dxTextSpace = 5 + self.Legend.dy = 5 + self.Legend.dx = 5 + self.Legend.deltay = 5 + self.Legend.alignment ='right' + self.chart.lineLabelFormat = None + self.chart.xLabel = 'X Axis' + self.chart.y = 30 + self.chart.yLabel = 'Y Axis' + self.chart.yValueAxis.labelTextFormat = '%d' + self.chart.yValueAxis.forceZero = 1 + self.chart.xValueAxis.forceZero = 1 + + + self._add(self,0,name='preview',validate=None,desc=None) + +if __name__=="__main__": #NORUNTESTS + Bubble().save(formats=['pdf'],outDir=None,fnRoot='bubble') diff --git a/bin/reportlab/graphics/samples/clustered_bar.py b/bin/reportlab/graphics/samples/clustered_bar.py new file mode 100644 index 00000000000..4d8c363eb29 --- /dev/null +++ b/bin/reportlab/graphics/samples/clustered_bar.py @@ -0,0 +1,84 @@ +#Autogenerated by ReportLab guiedit do not edit +from reportlab.graphics.charts.legends import Legend +from excelcolors import * +from reportlab.graphics.charts.barcharts import HorizontalBarChart +from reportlab.graphics.shapes import Drawing, _DrawingEditorMixin, String +from reportlab.graphics.charts.textlabels import Label + +class ClusteredBar(_DrawingEditorMixin,Drawing): + def __init__(self,width=200,height=150,*args,**kw): + apply(Drawing.__init__,(self,width,height)+args,kw) + self._add(self,HorizontalBarChart(),name='chart',validate=None,desc="The main chart") + self.chart.width = 115 + self.chart.height = 80 + self.chart.x = 30 + self.chart.y = 40 + self.chart.bars[0].fillColor = color01 + self.chart.bars[1].fillColor = color02 + self.chart.bars[2].fillColor = color03 + self.chart.bars[3].fillColor = color04 + self.chart.bars[4].fillColor = color05 + self.chart.bars[5].fillColor = color06 + self.chart.bars[6].fillColor = color07 + self.chart.bars[7].fillColor = color08 + self.chart.bars[8].fillColor = color09 + self.chart.bars[9].fillColor = color10 + self.chart.fillColor = backgroundGrey + self.chart.barLabels.fontName = 'Helvetica' + self.chart.valueAxis.labels.fontName = 'Helvetica' + self.chart.valueAxis.labels.fontSize = 6 + self.chart.valueAxis.forceZero = 1 + self.chart.data = [(100, 150, 180), (125, 180, 200)] + self.chart.groupSpacing = 15 + self.chart.valueAxis.avoidBoundFrac = 1 + self.chart.valueAxis.gridEnd = 80 + self.chart.valueAxis.tickDown = 3 + self.chart.valueAxis.visibleGrid = 1 + self.chart.categoryAxis.categoryNames = ['North', 'South', 'Central'] + self.chart.categoryAxis.tickLeft = 3 + self.chart.categoryAxis.labels.fontName = 'Helvetica' + self.chart.categoryAxis.labels.fontSize = 6 + self.chart.categoryAxis.labels.dx = -3 + self._add(self,Label(),name='Title',validate=None,desc="The title at the top of the chart") + self.Title.fontName = 'Helvetica-Bold' + self.Title.fontSize = 7 + self.Title.x = 100 + self.Title.y = 135 + self.Title._text = 'Chart Title' + self.Title.maxWidth = 180 + self.Title.height = 20 + self.Title.textAnchor ='middle' + self._add(self,Legend(),name='Legend',validate=None,desc="The legend or key for the chart") + self.Legend.colorNamePairs = [(color01, 'Widgets'), (color02, 'Sprockets')] + self.Legend.fontName = 'Helvetica' + self.Legend.fontSize = 7 + self.Legend.x = 153 + self.Legend.y = 85 + self.Legend.dxTextSpace = 5 + self.Legend.dy = 5 + self.Legend.dx = 5 + self.Legend.deltay = 5 + self.Legend.alignment ='right' + self._add(self,Label(),name='XLabel',validate=None,desc="The label on the horizontal axis") + self.XLabel.fontName = 'Helvetica' + self.XLabel.fontSize = 7 + self.XLabel.x = 85 + self.XLabel.y = 10 + self.XLabel.textAnchor ='middle' + self.XLabel.maxWidth = 100 + self.XLabel.height = 20 + self.XLabel._text = "X Axis" + self._add(self,Label(),name='YLabel',validate=None,desc="The label on the vertical axis") + self.YLabel.fontName = 'Helvetica' + self.YLabel.fontSize = 7 + self.YLabel.x = 12 + self.YLabel.y = 80 + self.YLabel.angle = 90 + self.YLabel.textAnchor ='middle' + self.YLabel.maxWidth = 100 + self.YLabel.height = 20 + self.YLabel._text = "Y Axis" + self._add(self,0,name='preview',validate=None,desc=None) + +if __name__=="__main__": #NORUNTESTS + ClusteredBar().save(formats=['pdf'],outDir=None,fnRoot='clustered_bar')
\ No newline at end of file diff --git a/bin/reportlab/graphics/samples/clustered_column.py b/bin/reportlab/graphics/samples/clustered_column.py new file mode 100644 index 00000000000..8ea9542eadc --- /dev/null +++ b/bin/reportlab/graphics/samples/clustered_column.py @@ -0,0 +1,83 @@ +#Autogenerated by ReportLab guiedit do not edit +from reportlab.graphics.charts.legends import Legend +from excelcolors import * +from reportlab.graphics.charts.barcharts import VerticalBarChart +from reportlab.graphics.shapes import Drawing, _DrawingEditorMixin, String +from reportlab.graphics.charts.textlabels import Label + +class ClusteredColumn(_DrawingEditorMixin,Drawing): + def __init__(self,width=200,height=150,*args,**kw): + apply(Drawing.__init__,(self,width,height)+args,kw) + self._add(self,VerticalBarChart(),name='chart',validate=None,desc="The main chart") + self.chart.width = 115 + self.chart.height = 80 + self.chart.x = 30 + self.chart.y = 40 + self.chart.bars[0].fillColor = color01 + self.chart.bars[1].fillColor = color02 + self.chart.bars[2].fillColor = color03 + self.chart.bars[3].fillColor = color04 + self.chart.bars[4].fillColor = color05 + self.chart.bars[5].fillColor = color06 + self.chart.bars[6].fillColor = color07 + self.chart.bars[7].fillColor = color08 + self.chart.bars[8].fillColor = color09 + self.chart.bars[9].fillColor = color10 + self.chart.fillColor = backgroundGrey + self.chart.barLabels.fontName = 'Helvetica' + self.chart.valueAxis.labels.fontName = 'Helvetica' + self.chart.valueAxis.labels.fontSize = 7 + self.chart.valueAxis.forceZero = 1 + self.chart.data = [(100, 150, 180), (125, 180, 200)] + self.chart.groupSpacing = 15 + self.chart.valueAxis.avoidBoundFrac = 1 + self.chart.valueAxis.gridEnd = 115 + self.chart.valueAxis.tickLeft = 3 + self.chart.valueAxis.visibleGrid = 1 + self.chart.categoryAxis.categoryNames = ['North', 'South', 'Central'] + self.chart.categoryAxis.tickDown = 3 + self.chart.categoryAxis.labels.fontName = 'Helvetica' + self.chart.categoryAxis.labels.fontSize = 7 + self._add(self,Label(),name='Title',validate=None,desc="The title at the top of the chart") + self.Title.fontName = 'Helvetica-Bold' + self.Title.fontSize = 7 + self.Title.x = 100 + self.Title.y = 135 + self.Title._text = 'Chart Title' + self.Title.maxWidth = 180 + self.Title.height = 20 + self.Title.textAnchor ='middle' + self._add(self,Legend(),name='Legend',validate=None,desc="The legend or key for the chart") + self.Legend.colorNamePairs = [(color01, 'Widgets'), (color02, 'Sprockets')] + self.Legend.fontName = 'Helvetica' + self.Legend.fontSize = 7 + self.Legend.x = 153 + self.Legend.y = 85 + self.Legend.dxTextSpace = 5 + self.Legend.dy = 5 + self.Legend.dx = 5 + self.Legend.deltay = 5 + self.Legend.alignment ='right' + self._add(self,Label(),name='XLabel',validate=None,desc="The label on the horizontal axis") + self.XLabel.fontName = 'Helvetica' + self.XLabel.fontSize = 7 + self.XLabel.x = 85 + self.XLabel.y = 10 + self.XLabel.textAnchor ='middle' + self.XLabel.maxWidth = 100 + self.XLabel.height = 20 + self.XLabel._text = "X Axis" + self._add(self,Label(),name='YLabel',validate=None,desc="The label on the vertical axis") + self.YLabel.fontName = 'Helvetica' + self.YLabel.fontSize = 7 + self.YLabel.x = 12 + self.YLabel.y = 80 + self.YLabel.angle = 90 + self.YLabel.textAnchor ='middle' + self.YLabel.maxWidth = 100 + self.YLabel.height = 20 + self.YLabel._text = "Y Axis" + self._add(self,0,name='preview',validate=None,desc=None) + +if __name__=="__main__": #NORUNTESTS + ClusteredColumn().save(formats=['pdf'],outDir=None,fnRoot='clustered_column') diff --git a/bin/reportlab/graphics/samples/excelcolors.py b/bin/reportlab/graphics/samples/excelcolors.py new file mode 100644 index 00000000000..f3f4f5b6950 --- /dev/null +++ b/bin/reportlab/graphics/samples/excelcolors.py @@ -0,0 +1,45 @@ +# define standard colors to mimic those used by Microsoft Excel +from reportlab.lib.colors import CMYKColor, PCMYKColor + +#colour names as comments at the end of each line are as a memory jogger ONLY +#NOT HTML named colours! + +#Main colours as used for bars etc +color01 = PCMYKColor(40,40,0,0) # Lavender +color02 = PCMYKColor(0,66,33,39) # Maroon +color03 = PCMYKColor(0,0,20,0) # Yellow +color04 = PCMYKColor(20,0,0,0) # Cyan +color05 = PCMYKColor(0,100,0,59) # Purple +color06 = PCMYKColor(0,49,49,0) # Salmon +color07 = PCMYKColor(100,49,0,19) # Blue +color08 = PCMYKColor(20,20,0,0) # PaleLavender +color09 = PCMYKColor(100,100,0,49) # NavyBlue +color10 = PCMYKColor(0,100,0,0) # Purple + +#Highlight colors - eg for the tops of bars +color01Light = PCMYKColor(39,39,0,25) # Light Lavender +color02Light = PCMYKColor(0,66,33,54) # Light Maroon +color03Light = PCMYKColor(0,0,19,25) # Light Yellow +color04Light = PCMYKColor(19,0,0,25) # Light Cyan +color05Light = PCMYKColor(0,100,0,69) # Light Purple +color06Light = PCMYKColor(0,49,49,25) # Light Salmon +color07Light = PCMYKColor(100,49,0,39) # Light Blue +color08Light = PCMYKColor(19,19,0,25) # Light PaleLavender +color09Light = PCMYKColor(100,100,0,62) # Light NavyBlue +color10Light = PCMYKColor(0,100,0,25) # Light Purple + +#Lowlight colors - eg for the sides of bars +color01Dark = PCMYKColor(39,39,0,49) # Dark Lavender +color02Dark = PCMYKColor(0,66,33,69) # Dark Maroon +color03Dark = PCMYKColor(0,0,20,49) # Dark Yellow +color04Dark = PCMYKColor(20,0,0,49) # Dark Cyan +color05Dark = PCMYKColor(0,100,0,80) # Dark Purple +color06Dark = PCMYKColor(0,50,50,49) # Dark Salmon +color07Dark = PCMYKColor(100,50,0,59) # Dark Blue +color08Dark = PCMYKColor(20,20,0,49) # Dark PaleLavender +color09Dark = PCMYKColor(100,100,0,79) # Dark NavyBlue +color10Dark = PCMYKColor(0,100,0,49) # Dark Purple + +#for standard grey backgrounds +backgroundGrey = PCMYKColor(0,0,0,24) + diff --git a/bin/reportlab/graphics/samples/exploded_pie.py b/bin/reportlab/graphics/samples/exploded_pie.py new file mode 100644 index 00000000000..8076493bd82 --- /dev/null +++ b/bin/reportlab/graphics/samples/exploded_pie.py @@ -0,0 +1,65 @@ +#Autogenerated by ReportLab guiedit do not edit +from reportlab.graphics.charts.piecharts import Pie +from excelcolors import * +from reportlab.graphics.widgets.grids import ShadedRect +from reportlab.graphics.charts.legends import Legend +from reportlab.graphics.shapes import Drawing, _DrawingEditorMixin, String +from reportlab.graphics.charts.textlabels import Label + +class ExplodedPie(_DrawingEditorMixin,Drawing): + def __init__(self,width=200,height=150,*args,**kw): + apply(Drawing.__init__,(self,width,height)+args,kw) + self._add(self,Pie(),name='chart',validate=None,desc="The main chart") + self.chart.width = 100 + self.chart.height = 100 + self.chart.x = 25 + self.chart.y = 25 + self.chart.slices[0].fillColor = color01 + self.chart.slices[1].fillColor = color02 + self.chart.slices[2].fillColor = color03 + self.chart.slices[3].fillColor = color04 + self.chart.slices[4].fillColor = color05 + self.chart.slices[5].fillColor = color06 + self.chart.slices[6].fillColor = color07 + self.chart.slices[7].fillColor = color08 + self.chart.slices[8].fillColor = color09 + self.chart.slices[9].fillColor = color10 + self.chart.data = (100, 150, 180) + self.chart.startAngle = -90 + self._add(self,Label(),name='Title',validate=None,desc="The title at the top of the chart") + self.Title.fontName = 'Helvetica-Bold' + self.Title.fontSize = 7 + self.Title.x = 100 + self.Title.y = 135 + self.Title._text = 'Chart Title' + self.Title.maxWidth = 180 + self.Title.height = 20 + self.Title.textAnchor ='middle' + self._add(self,Legend(),name='Legend',validate=None,desc="The legend or key for the chart") + self.Legend.colorNamePairs = [(color01, 'North'), (color02, 'South'), (color03, 'Central')] + self.Legend.fontName = 'Helvetica' + self.Legend.fontSize = 7 + self.Legend.x = 160 + self.Legend.y = 85 + self.Legend.dxTextSpace = 5 + self.Legend.dy = 5 + self.Legend.dx = 5 + self.Legend.deltay = 5 + self.Legend.alignment ='right' + self.Legend.columnMaximum = 10 + self.chart.slices.strokeWidth = 1 + self.chart.slices.fontName = 'Helvetica' + self.background = ShadedRect() + self.background.fillColorStart = backgroundGrey + self.background.fillColorEnd = backgroundGrey + self.background.numShades = 1 + self.background.strokeWidth = 0.5 + self.background.x = 20 + self.background.y = 20 + self.chart.slices.popout = 5 + self.background.height = 110 + self.background.width = 110 + self._add(self,0,name='preview',validate=None,desc=None) + +if __name__=="__main__": #NORUNTESTS + ExplodedPie().save(formats=['pdf'],outDir=None,fnRoot='exploded_pie')
\ No newline at end of file diff --git a/bin/reportlab/graphics/samples/filled_radar.py b/bin/reportlab/graphics/samples/filled_radar.py new file mode 100644 index 00000000000..ba66662a239 --- /dev/null +++ b/bin/reportlab/graphics/samples/filled_radar.py @@ -0,0 +1,54 @@ +#Autogenerated by ReportLab guiedit do not edit +from reportlab.graphics.charts.legends import Legend +from reportlab.graphics.charts.spider import SpiderChart +from reportlab.graphics.shapes import Drawing, _DrawingEditorMixin, String +from reportlab.graphics.charts.textlabels import Label +from excelcolors import * + +class FilledRadarChart(_DrawingEditorMixin,Drawing): + def __init__(self,width=200,height=150,*args,**kw): + apply(Drawing.__init__,(self,width,height)+args,kw) + self._add(self,SpiderChart(),name='chart',validate=None,desc="The main chart") + self.chart.width = 90 + self.chart.height = 90 + self.chart.x = 45 + self.chart.y = 25 + self.chart.strands[0].fillColor = color01 + self.chart.strands[1].fillColor = color02 + self.chart.strands[2].fillColor = color03 + self.chart.strands[3].fillColor = color04 + self.chart.strands[4].fillColor = color05 + self.chart.strands[5].fillColor = color06 + self.chart.strands[6].fillColor = color07 + self.chart.strands[7].fillColor = color08 + self.chart.strands[8].fillColor = color09 + self.chart.strands[9].fillColor = color10 + self.chart.strands.fontName = 'Helvetica' + self.chart.strands.fontSize = 6 + self.chart.fillColor = backgroundGrey + self.chart.data = [(125, 180, 200), (100, 150, 180)] + self.chart.labels = ['North', 'South', 'Central'] + self._add(self,Label(),name='Title',validate=None,desc="The title at the top of the chart") + self.Title.fontName = 'Helvetica-Bold' + self.Title.fontSize = 7 + self.Title.x = 100 + self.Title.y = 135 + self.Title._text = 'Chart Title' + self.Title.maxWidth = 180 + self.Title.height = 20 + self.Title.textAnchor ='middle' + self._add(self,Legend(),name='Legend',validate=None,desc="The legend or key for the chart") + self.Legend.colorNamePairs = [(color01, 'Widgets'), (color02, 'Sprockets')] + self.Legend.fontName = 'Helvetica' + self.Legend.fontSize = 7 + self.Legend.x = 153 + self.Legend.y = 85 + self.Legend.dxTextSpace = 5 + self.Legend.dy = 5 + self.Legend.dx = 5 + self.Legend.deltay = 5 + self.Legend.alignment ='right' + self._add(self,0,name='preview',validate=None,desc=None) + +if __name__=="__main__": #NORUNTESTS + FilledRadarChart().save(formats=['pdf'],outDir=None,fnRoot='filled_radar')
\ No newline at end of file diff --git a/bin/reportlab/graphics/samples/line_chart.py b/bin/reportlab/graphics/samples/line_chart.py new file mode 100644 index 00000000000..49563a94766 --- /dev/null +++ b/bin/reportlab/graphics/samples/line_chart.py @@ -0,0 +1,83 @@ +#Autogenerated by ReportLab guiedit do not edit +from reportlab.graphics.charts.legends import Legend +from reportlab.graphics.charts.lineplots import LinePlot +from reportlab.graphics.shapes import Drawing, _DrawingEditorMixin, String +from reportlab.graphics.charts.textlabels import Label +from excelcolors import * + +class LineChart(_DrawingEditorMixin,Drawing): + def __init__(self,width=200,height=150,*args,**kw): + apply(Drawing.__init__,(self,width,height)+args,kw) + self._add(self,LinePlot(),name='chart',validate=None,desc="The main chart") + self.chart.width = 115 + self.chart.height = 80 + self.chart.x = 30 + self.chart.y = 40 + self.chart.lines[0].strokeColor = color01 + self.chart.lines[1].strokeColor = color02 + self.chart.lines[2].strokeColor = color03 + self.chart.lines[3].strokeColor = color04 + self.chart.lines[4].strokeColor = color05 + self.chart.lines[5].strokeColor = color06 + self.chart.lines[6].strokeColor = color07 + self.chart.lines[7].strokeColor = color08 + self.chart.lines[8].strokeColor = color09 + self.chart.lines[9].strokeColor = color10 + self.chart.fillColor = backgroundGrey + self.chart.lineLabels.fontName = 'Helvetica' + self.chart.xValueAxis.labels.fontName = 'Helvetica' + self.chart.xValueAxis.labels.fontSize = 7 + self.chart.xValueAxis.forceZero = 0 + self.chart.data = [((0, 50), (100,100), (200,200), (250,210), (300,300), (400,500)), ((0, 150), (100,200), (200,300), (250,200), (300,400), (400, 600))] + self.chart.xValueAxis.avoidBoundFrac = 1 + self.chart.xValueAxis.gridEnd = 115 + self.chart.xValueAxis.tickDown = 3 + self.chart.xValueAxis.visibleGrid = 1 + self.chart.yValueAxis.tickLeft = 3 + self.chart.yValueAxis.labels.fontName = 'Helvetica' + self.chart.yValueAxis.labels.fontSize = 7 + self._add(self,Label(),name='Title',validate=None,desc="The title at the top of the chart") + self.Title.fontName = 'Helvetica-Bold' + self.Title.fontSize = 7 + self.Title.x = 100 + self.Title.y = 135 + self.Title._text = 'Chart Title' + self.Title.maxWidth = 180 + self.Title.height = 20 + self.Title.textAnchor ='middle' + self._add(self,Legend(),name='Legend',validate=None,desc="The legend or key for the chart") + self.Legend.colorNamePairs = [(color01, 'Widgets'), (color02, 'Sprockets')] + self.Legend.fontName = 'Helvetica' + self.Legend.fontSize = 7 + self.Legend.x = 153 + self.Legend.y = 85 + self.Legend.dxTextSpace = 5 + self.Legend.dy = 5 + self.Legend.dx = 5 + self.Legend.deltay = 5 + self.Legend.alignment ='right' + self._add(self,Label(),name='XLabel',validate=None,desc="The label on the horizontal axis") + self.XLabel.fontName = 'Helvetica' + self.XLabel.fontSize = 7 + self.XLabel.x = 85 + self.XLabel.y = 10 + self.XLabel.textAnchor ='middle' + self.XLabel.maxWidth = 100 + self.XLabel.height = 20 + self.XLabel._text = "X Axis" + self._add(self,Label(),name='YLabel',validate=None,desc="The label on the vertical axis") + self.YLabel.fontName = 'Helvetica' + self.YLabel.fontSize = 7 + self.YLabel.x = 12 + self.YLabel.y = 80 + self.YLabel.angle = 90 + self.YLabel.textAnchor ='middle' + self.YLabel.maxWidth = 100 + self.YLabel.height = 20 + self.YLabel._text = "Y Axis" + self.chart.yValueAxis.forceZero = 1 + self.chart.xValueAxis.forceZero = 1 + self._add(self,0,name='preview',validate=None,desc=None) + +if __name__=="__main__": #NORUNTESTS + LineChart().save(formats=['pdf'],outDir=None,fnRoot='line_chart') diff --git a/bin/reportlab/graphics/samples/linechart_with_markers.py b/bin/reportlab/graphics/samples/linechart_with_markers.py new file mode 100644 index 00000000000..2875cecb5d7 --- /dev/null +++ b/bin/reportlab/graphics/samples/linechart_with_markers.py @@ -0,0 +1,94 @@ +#Autogenerated by ReportLab guiedit do not edit +from reportlab.graphics.charts.legends import Legend +from reportlab.graphics.charts.lineplots import LinePlot +from reportlab.graphics.shapes import Drawing, _DrawingEditorMixin, String +from reportlab.graphics.widgets.markers import makeMarker +from reportlab.graphics.charts.textlabels import Label +from excelcolors import * + +class LineChartWithMarkers(_DrawingEditorMixin,Drawing): + def __init__(self,width=200,height=150,*args,**kw): + apply(Drawing.__init__,(self,width,height)+args,kw) + self._add(self,LinePlot(),name='chart',validate=None,desc="The main chart") + self.chart.width = 115 + self.chart.height = 80 + self.chart.x = 30 + self.chart.y = 40 + self.chart.lines[0].strokeColor = color01 + self.chart.lines[1].strokeColor = color02 + self.chart.lines[2].strokeColor = color03 + self.chart.lines[3].strokeColor = color04 + self.chart.lines[4].strokeColor = color05 + self.chart.lines[5].strokeColor = color06 + self.chart.lines[6].strokeColor = color07 + self.chart.lines[7].strokeColor = color08 + self.chart.lines[8].strokeColor = color09 + self.chart.lines[9].strokeColor = color10 + self.chart.lines[0].symbol = makeMarker('FilledSquare') + self.chart.lines[1].symbol = makeMarker('FilledDiamond') + self.chart.lines[2].symbol = makeMarker('FilledStarFive') + self.chart.lines[3].symbol = makeMarker('FilledTriangle') + self.chart.lines[4].symbol = makeMarker('FilledCircle') + self.chart.lines[5].symbol = makeMarker('FilledPentagon') + self.chart.lines[6].symbol = makeMarker('FilledStarSix') + self.chart.lines[7].symbol = makeMarker('FilledHeptagon') + self.chart.lines[8].symbol = makeMarker('FilledOctagon') + self.chart.lines[9].symbol = makeMarker('FilledCross') + self.chart.fillColor = backgroundGrey + self.chart.lineLabels.fontName = 'Helvetica' + self.chart.xValueAxis.labels.fontName = 'Helvetica' + self.chart.xValueAxis.labels.fontSize = 7 + self.chart.xValueAxis.forceZero = 0 + self.chart.data = [((0, 50), (100,100), (200,200), (250,210), (300,300), (400,500)), ((0, 150), (100,200), (200,300), (250,200), (300,400), (400, 600))] + self.chart.xValueAxis.avoidBoundFrac = 1 + self.chart.xValueAxis.gridEnd = 115 + self.chart.xValueAxis.tickDown = 3 + self.chart.xValueAxis.visibleGrid = 1 + self.chart.yValueAxis.tickLeft = 3 + self.chart.yValueAxis.labels.fontName = 'Helvetica' + self.chart.yValueAxis.labels.fontSize = 7 + self._add(self,Label(),name='Title',validate=None,desc="The title at the top of the chart") + self.Title.fontName = 'Helvetica-Bold' + self.Title.fontSize = 7 + self.Title.x = 100 + self.Title.y = 135 + self.Title._text = 'Chart Title' + self.Title.maxWidth = 180 + self.Title.height = 20 + self.Title.textAnchor ='middle' + self._add(self,Legend(),name='Legend',validate=None,desc="The legend or key for the chart") + self.Legend.colorNamePairs = [(color01, 'Widgets'), (color02, 'Sprockets')] + self.Legend.fontName = 'Helvetica' + self.Legend.fontSize = 7 + self.Legend.x = 153 + self.Legend.y = 85 + self.Legend.dxTextSpace = 5 + self.Legend.dy = 5 + self.Legend.dx = 5 + self.Legend.deltay = 5 + self.Legend.alignment ='right' + self._add(self,Label(),name='XLabel',validate=None,desc="The label on the horizontal axis") + self.XLabel.fontName = 'Helvetica' + self.XLabel.fontSize = 7 + self.XLabel.x = 85 + self.XLabel.y = 10 + self.XLabel.textAnchor ='middle' + self.XLabel.maxWidth = 100 + self.XLabel.height = 20 + self.XLabel._text = "X Axis" + self._add(self,Label(),name='YLabel',validate=None,desc="The label on the vertical axis") + self.YLabel.fontName = 'Helvetica' + self.YLabel.fontSize = 7 + self.YLabel.x = 12 + self.YLabel.y = 80 + self.YLabel.angle = 90 + self.YLabel.textAnchor ='middle' + self.YLabel.maxWidth = 100 + self.YLabel.height = 20 + self.YLabel._text = "Y Axis" + self.chart.yValueAxis.forceZero = 1 + self.chart.xValueAxis.forceZero = 1 + self._add(self,0,name='preview',validate=None,desc=None) + +if __name__=="__main__": #NORUNTESTS + LineChartWithMarkers().save(formats=['pdf'],outDir=None,fnRoot='linechart_with_markers') diff --git a/bin/reportlab/graphics/samples/radar.py b/bin/reportlab/graphics/samples/radar.py new file mode 100644 index 00000000000..2d61d091ab9 --- /dev/null +++ b/bin/reportlab/graphics/samples/radar.py @@ -0,0 +1,66 @@ +#Autogenerated by ReportLab guiedit do not edit +from reportlab.graphics.charts.legends import Legend +from excelcolors import * +from reportlab.graphics.charts.spider import SpiderChart +from reportlab.graphics.shapes import Drawing, _DrawingEditorMixin, String +from reportlab.graphics.charts.textlabels import Label + +class RadarChart(_DrawingEditorMixin,Drawing): + def __init__(self,width=200,height=150,*args,**kw): + apply(Drawing.__init__,(self,width,height)+args,kw) + self._add(self,SpiderChart(),name='chart',validate=None,desc="The main chart") + self.chart.width = 90 + self.chart.height = 90 + self.chart.x = 45 + self.chart.y = 25 + self.chart.strands[0].strokeColor= color01 + self.chart.strands[1].strokeColor= color02 + self.chart.strands[2].strokeColor= color03 + self.chart.strands[3].strokeColor= color04 + self.chart.strands[4].strokeColor= color05 + self.chart.strands[5].strokeColor= color06 + self.chart.strands[6].strokeColor= color07 + self.chart.strands[7].strokeColor= color08 + self.chart.strands[8].strokeColor= color09 + self.chart.strands[9].strokeColor= color10 + self.chart.strands[0].fillColor = None + self.chart.strands[1].fillColor = None + self.chart.strands[2].fillColor = None + self.chart.strands[3].fillColor = None + self.chart.strands[4].fillColor = None + self.chart.strands[5].fillColor = None + self.chart.strands[6].fillColor = None + self.chart.strands[7].fillColor = None + self.chart.strands[8].fillColor = None + self.chart.strands[9].fillColor = None + self.chart.strands.strokeWidth = 1 + self.chart.strands.fontName = 'Helvetica' + self.chart.strands.fontSize = 6 + self.chart.fillColor = backgroundGrey + self.chart.data = [(125, 180, 200), (100, 150, 180)] + self.chart.labels = ['North', 'South', 'Central'] + self._add(self,Label(),name='Title',validate=None,desc="The title at the top of the chart") + self.Title.fontName = 'Helvetica-Bold' + self.Title.fontSize = 7 + self.Title.x = 100 + self.Title.y = 135 + self.Title._text = 'Chart Title' + self.Title.maxWidth = 180 + self.Title.height = 20 + self.Title.textAnchor ='middle' + self._add(self,Legend(),name='Legend',validate=None,desc="The legend or key for the chart") + self.Legend.colorNamePairs = [(color01, 'Widgets'), (color02, 'Sprockets')] + self.Legend.fontName = 'Helvetica' + self.Legend.fontSize = 7 + self.Legend.x = 153 + self.Legend.y = 85 + self.Legend.dxTextSpace = 5 + self.Legend.dy = 5 + self.Legend.dx = 5 + self.Legend.deltay = 5 + self.Legend.alignment ='right' + self.chart.strands.strokeWidth = 1 + self._add(self,0,name='preview',validate=None,desc=None) + +if __name__=="__main__": #NORUNTESTS + RadarChart().save(formats=['pdf'],outDir=None,fnRoot='radar')
\ No newline at end of file diff --git a/bin/reportlab/graphics/samples/runall.py b/bin/reportlab/graphics/samples/runall.py new file mode 100644 index 00000000000..938ab8fec42 --- /dev/null +++ b/bin/reportlab/graphics/samples/runall.py @@ -0,0 +1,59 @@ +# runs all the GUIedit charts in this directory - +# makes a PDF sample for eaxh existing chart type +import sys +import glob +import string +import inspect +import types + +def moduleClasses(mod): + def P(obj, m=mod.__name__, CT=types.ClassType): + return (type(obj)==CT and obj.__module__==m) + try: + return inspect.getmembers(mod, P)[0][1] + except: + return None + +def getclass(f): + return moduleClasses(__import__(f)) + +def run(format, VERBOSE=0): + formats = string.split(format, ',') + for i in range(0, len(formats)): + formats[i] == string.lower(string.strip(formats[i])) + allfiles = glob.glob('*.py') + allfiles.sort() + for fn in allfiles: + f = string.split(fn, '.')[0] + c = getclass(f) + if c != None: + print c.__name__ + try: + for fmt in formats: + if fmt: + c().save(formats=[fmt],outDir='.',fnRoot=c.__name__) + if VERBOSE: + print " %s.%s" % (c.__name__, fmt) + except: + print " COULDN'T CREATE '%s.%s'!" % (c.__name__, format) + +if __name__ == "__main__": + if len(sys.argv) == 1: + run('pdf,pict,png') + else: + try: + if sys.argv[1] == "-h": + print 'usage: runall.py [FORMAT] [-h]' + print ' if format is supplied is should be one or more of pdf,gif,eps,png etc' + print ' if format is missing the following formats are assumed: pdf,pict,png' + print ' -h prints this message' + else: + t = sys.argv[1:] + for f in t: + run(f) + except: + print 'usage: runall.py [FORMAT][-h]' + print ' if format is supplied is should be one or more of pdf,gif,eps,png etc' + print ' if format is missing the following formats are assumed: pdf,pict,png' + print ' -h prints this message' + raise diff --git a/bin/reportlab/graphics/samples/scatter.py b/bin/reportlab/graphics/samples/scatter.py new file mode 100644 index 00000000000..ea1a9991d3e --- /dev/null +++ b/bin/reportlab/graphics/samples/scatter.py @@ -0,0 +1,71 @@ +#Autogenerated by ReportLab guiedit do not edit +from reportlab.graphics.charts.legends import Legend +from reportlab.graphics.charts.lineplots import ScatterPlot +from reportlab.graphics.shapes import Drawing, _DrawingEditorMixin, String +from reportlab.graphics.charts.textlabels import Label +from excelcolors import * + +class Scatter(_DrawingEditorMixin,Drawing): + def __init__(self,width=200,height=150,*args,**kw): + apply(Drawing.__init__,(self,width,height)+args,kw) + self._add(self,ScatterPlot(),name='chart',validate=None,desc="The main chart") + self.chart.width = 115 + self.chart.height = 80 + self.chart.x = 30 + self.chart.y = 40 + self.chart.lines[0].strokeColor = color01 + self.chart.lines[1].strokeColor = color02 + self.chart.lines[2].strokeColor = color03 + self.chart.lines[3].strokeColor = color04 + self.chart.lines[4].strokeColor = color05 + self.chart.lines[5].strokeColor = color06 + self.chart.lines[6].strokeColor = color07 + self.chart.lines[7].strokeColor = color08 + self.chart.lines[8].strokeColor = color09 + self.chart.lines[9].strokeColor = color10 + self.chart.fillColor = backgroundGrey + self.chart.lineLabels.fontName = 'Helvetica' + self.chart.xValueAxis.labels.fontName = 'Helvetica' + self.chart.xValueAxis.labels.fontSize = 7 + self.chart.xValueAxis.forceZero = 0 + self.chart.data = [((100,100), (200,200), (250,210), (300,300), (400,500)), ((100,200), (200,300), (250,200), (300,400), (400, 600))] + self.chart.xValueAxis.avoidBoundFrac = 1 + self.chart.xValueAxis.gridEnd = 115 + self.chart.xValueAxis.tickDown = 3 + self.chart.xValueAxis.visibleGrid = 1 + self.chart.yValueAxis.tickLeft = 3 + self.chart.yValueAxis.labels.fontName = 'Helvetica' + self.chart.yValueAxis.labels.fontSize = 7 + self._add(self,Label(),name='Title',validate=None,desc="The title at the top of the chart") + self.Title.fontName = 'Helvetica-Bold' + self.Title.fontSize = 7 + self.Title.x = 100 + self.Title.y = 135 + self.Title._text = 'Chart Title' + self.Title.maxWidth = 180 + self.Title.height = 20 + self.Title.textAnchor ='middle' + self._add(self,Legend(),name='Legend',validate=None,desc="The legend or key for the chart") + self.Legend.colorNamePairs = [(color01, 'Widgets'), (color02, 'Sprockets')] + self.Legend.fontName = 'Helvetica' + self.Legend.fontSize = 7 + self.Legend.x = 153 + self.Legend.y = 85 + self.Legend.dxTextSpace = 5 + self.Legend.dy = 5 + self.Legend.dx = 5 + self.Legend.deltay = 5 + self.Legend.alignment ='right' + self.chart.lineLabelFormat = None + self.chart.xLabel = 'X Axis' + self.chart.y = 30 + self.chart.yLabel = 'Y Axis' + self.chart.yValueAxis.labelTextFormat = '%d' + self.chart.yValueAxis.forceZero = 1 + self.chart.xValueAxis.forceZero = 1 + + + self._add(self,0,name='preview',validate=None,desc=None) + +if __name__=="__main__": #NORUNTESTS + Scatter().save(formats=['pdf'],outDir=None,fnRoot='scatter') diff --git a/bin/reportlab/graphics/samples/scatter_lines.py b/bin/reportlab/graphics/samples/scatter_lines.py new file mode 100644 index 00000000000..16ed718b2fd --- /dev/null +++ b/bin/reportlab/graphics/samples/scatter_lines.py @@ -0,0 +1,82 @@ +#Autogenerated by ReportLab guiedit do not edit +from reportlab.graphics.charts.legends import Legend +from reportlab.graphics.charts.lineplots import ScatterPlot +from reportlab.graphics.shapes import Drawing, _DrawingEditorMixin, String +from reportlab.graphics.charts.textlabels import Label +from excelcolors import * + +class ScatterLines(_DrawingEditorMixin,Drawing): + def __init__(self,width=200,height=150,*args,**kw): + apply(Drawing.__init__,(self,width,height)+args,kw) + self._add(self,ScatterPlot(),name='chart',validate=None,desc="The main chart") + self.chart.width = 115 + self.chart.height = 80 + self.chart.x = 30 + self.chart.y = 40 + self.chart.lines[0].strokeColor = color01 + self.chart.lines[1].strokeColor = color02 + self.chart.lines[2].strokeColor = color03 + self.chart.lines[3].strokeColor = color04 + self.chart.lines[4].strokeColor = color05 + self.chart.lines[5].strokeColor = color06 + self.chart.lines[6].strokeColor = color07 + self.chart.lines[7].strokeColor = color08 + self.chart.lines[8].strokeColor = color09 + self.chart.lines[9].strokeColor = color10 + self.chart.lines[0].symbol = None + self.chart.lines[1].symbol = None + self.chart.lines[2].symbol = None + self.chart.lines[3].symbol = None + self.chart.lines[4].symbol = None + self.chart.lines[5].symbol = None + self.chart.lines[6].symbol = None + self.chart.lines[7].symbol = None + self.chart.lines[8].symbol = None + self.chart.lines[9].symbol = None + self.chart.fillColor = backgroundGrey + self.chart.lineLabels.fontName = 'Helvetica' + self.chart.xValueAxis.labels.fontName = 'Helvetica' + self.chart.xValueAxis.labels.fontSize = 7 + self.chart.xValueAxis.forceZero = 0 + self.chart.data = [((100,100), (200,200), (250,210), (300,300), (400,500)), ((100,200), (200,300), (250,200), (300,400), (400, 600))] + self.chart.xValueAxis.avoidBoundFrac = 1 + self.chart.xValueAxis.gridEnd = 115 + self.chart.xValueAxis.tickDown = 3 + self.chart.xValueAxis.visibleGrid = 1 + self.chart.yValueAxis.tickLeft = 3 + self.chart.yValueAxis.labels.fontName = 'Helvetica' + self.chart.yValueAxis.labels.fontSize = 7 + self._add(self,Label(),name='Title',validate=None,desc="The title at the top of the chart") + self.Title.fontName = 'Helvetica-Bold' + self.Title.fontSize = 7 + self.Title.x = 100 + self.Title.y = 135 + self.Title._text = 'Chart Title' + self.Title.maxWidth = 180 + self.Title.height = 20 + self.Title.textAnchor ='middle' + self._add(self,Legend(),name='Legend',validate=None,desc="The legend or key for the chart") + self.Legend.colorNamePairs = [(color01, 'Widgets'), (color02, 'Sprockets')] + self.Legend.fontName = 'Helvetica' + self.Legend.fontSize = 7 + self.Legend.x = 153 + self.Legend.y = 85 + self.Legend.dxTextSpace = 5 + self.Legend.dy = 5 + self.Legend.dx = 5 + self.Legend.deltay = 5 + self.Legend.alignment ='right' + self.chart.lineLabelFormat = None + self.chart.xLabel = 'X Axis' + self.chart.y = 30 + self.chart.yLabel = 'Y Axis' + self.chart.yValueAxis.gridEnd = 115 + self.chart.yValueAxis.visibleGrid = 1 + self.chart.yValueAxis.labelTextFormat = '%d' + self.chart.yValueAxis.forceZero = 1 + self.chart.xValueAxis.forceZero = 1 + self.chart.joinedLines = 1 + self._add(self,0,name='preview',validate=None,desc=None) + +if __name__=="__main__": #NORUNTESTS + ScatterLines().save(formats=['pdf'],outDir=None,fnRoot='scatter_lines') diff --git a/bin/reportlab/graphics/samples/scatter_lines_markers.py b/bin/reportlab/graphics/samples/scatter_lines_markers.py new file mode 100644 index 00000000000..34f8ff220f5 --- /dev/null +++ b/bin/reportlab/graphics/samples/scatter_lines_markers.py @@ -0,0 +1,72 @@ +#Autogenerated by ReportLab guiedit do not edit +from reportlab.graphics.charts.legends import Legend +from reportlab.graphics.charts.lineplots import ScatterPlot +from reportlab.graphics.shapes import Drawing, _DrawingEditorMixin, String +from reportlab.graphics.charts.textlabels import Label +from excelcolors import * + +class ScatterLinesMarkers(_DrawingEditorMixin,Drawing): + def __init__(self,width=200,height=150,*args,**kw): + apply(Drawing.__init__,(self,width,height)+args,kw) + self._add(self,ScatterPlot(),name='chart',validate=None,desc="The main chart") + self.chart.width = 115 + self.chart.height = 80 + self.chart.x = 30 + self.chart.y = 40 + self.chart.lines[0].strokeColor = color01 + self.chart.lines[1].strokeColor = color02 + self.chart.lines[2].strokeColor = color03 + self.chart.lines[3].strokeColor = color04 + self.chart.lines[4].strokeColor = color05 + self.chart.lines[5].strokeColor = color06 + self.chart.lines[6].strokeColor = color07 + self.chart.lines[7].strokeColor = color08 + self.chart.lines[8].strokeColor = color09 + self.chart.lines[9].strokeColor = color10 + self.chart.fillColor = backgroundGrey + self.chart.lineLabels.fontName = 'Helvetica' + self.chart.xValueAxis.labels.fontName = 'Helvetica' + self.chart.xValueAxis.labels.fontSize = 7 + self.chart.xValueAxis.forceZero = 0 + self.chart.data = [((100,100), (200,200), (250,210), (300,300), (400,500)), ((100,200), (200,300), (250,200), (300,400), (400, 600))] + self.chart.xValueAxis.avoidBoundFrac = 1 + self.chart.xValueAxis.gridEnd = 115 + self.chart.xValueAxis.tickDown = 3 + self.chart.xValueAxis.visibleGrid = 1 + self.chart.yValueAxis.tickLeft = 3 + self.chart.yValueAxis.labels.fontName = 'Helvetica' + self.chart.yValueAxis.labels.fontSize = 7 + self._add(self,Label(),name='Title',validate=None,desc="The title at the top of the chart") + self.Title.fontName = 'Helvetica-Bold' + self.Title.fontSize = 7 + self.Title.x = 100 + self.Title.y = 135 + self.Title._text = 'Chart Title' + self.Title.maxWidth = 180 + self.Title.height = 20 + self.Title.textAnchor ='middle' + self._add(self,Legend(),name='Legend',validate=None,desc="The legend or key for the chart") + self.Legend.colorNamePairs = [(color01, 'Widgets'), (color02, 'Sprockets')] + self.Legend.fontName = 'Helvetica' + self.Legend.fontSize = 7 + self.Legend.x = 153 + self.Legend.y = 85 + self.Legend.dxTextSpace = 5 + self.Legend.dy = 5 + self.Legend.dx = 5 + self.Legend.deltay = 5 + self.Legend.alignment ='right' + self.chart.lineLabelFormat = None + self.chart.xLabel = 'X Axis' + self.chart.y = 30 + self.chart.yLabel = 'Y Axis' + self.chart.yValueAxis.gridEnd = 115 + self.chart.yValueAxis.visibleGrid = 1 + self.chart.yValueAxis.labelTextFormat = '%d' + self.chart.yValueAxis.forceZero = 1 + self.chart.xValueAxis.forceZero = 1 + self.chart.joinedLines = 1 + self._add(self,0,name='preview',validate=None,desc=None) + +if __name__=="__main__": #NORUNTESTS + ScatterLinesMarkers().save(formats=['pdf'],outDir=None,fnRoot='scatter_lines_markers') diff --git a/bin/reportlab/graphics/samples/simple_pie.py b/bin/reportlab/graphics/samples/simple_pie.py new file mode 100644 index 00000000000..7542607f6ae --- /dev/null +++ b/bin/reportlab/graphics/samples/simple_pie.py @@ -0,0 +1,61 @@ +#Autogenerated by ReportLab guiedit do not edit +from reportlab.graphics.charts.piecharts import Pie +from reportlab.graphics.widgets.grids import ShadedRect +from reportlab.graphics.charts.legends import Legend +from reportlab.graphics.shapes import Drawing, _DrawingEditorMixin, String +from reportlab.graphics.charts.textlabels import Label +from excelcolors import * + +class SimplePie(_DrawingEditorMixin,Drawing): + def __init__(self,width=200,height=150,*args,**kw): + apply(Drawing.__init__,(self,width,height)+args,kw) + self._add(self,Pie(),name='chart',validate=None,desc="The main chart") + self.chart.width = 100 + self.chart.height = 100 + self.chart.x = 25 + self.chart.y = 25 + self.chart.slices[0].fillColor = color01 + self.chart.slices[1].fillColor = color02 + self.chart.slices[2].fillColor = color03 + self.chart.slices[3].fillColor = color04 + self.chart.slices[4].fillColor = color05 + self.chart.slices[5].fillColor = color06 + self.chart.slices[6].fillColor = color07 + self.chart.slices[7].fillColor = color08 + self.chart.slices[8].fillColor = color09 + self.chart.slices[9].fillColor = color10 + self.chart.data = (100, 150, 180) + self._add(self,Label(),name='Title',validate=None,desc="The title at the top of the chart") + self.Title.fontName = 'Helvetica-Bold' + self.Title.fontSize = 7 + self.Title.x = 100 + self.Title.y = 135 + self.Title._text = 'Chart Title' + self.Title.maxWidth = 180 + self.Title.height = 20 + self.Title.textAnchor ='middle' + self._add(self,Legend(),name='Legend',validate=None,desc="The legend or key for the chart") + self.Legend.colorNamePairs = [(color01, 'North'), (color02, 'South'),(color03, 'Central')] + self.Legend.fontName = 'Helvetica' + self.Legend.fontSize = 7 + self.Legend.x = 160 + self.Legend.y = 85 + self.Legend.dxTextSpace = 5 + self.Legend.dy = 5 + self.Legend.dx = 5 + self.Legend.deltay = 5 + self.Legend.alignment ='right' + self.chart.slices.strokeWidth = 1 + self.chart.slices.fontName = 'Helvetica' + self.background = ShadedRect() + self.background.fillColorStart = backgroundGrey + self.background.fillColorEnd = backgroundGrey + self.background.numShades = 1 + self.background.strokeWidth = 0.5 + self.background.x = 25 + self.background.y = 25 + self.Legend.columnMaximum = 10 + self._add(self,0,name='preview',validate=None,desc=None) + +if __name__=="__main__": #NORUNTESTS + SimplePie().save(formats=['pdf'],outDir=None,fnRoot=None)
\ No newline at end of file diff --git a/bin/reportlab/graphics/samples/stacked_bar.py b/bin/reportlab/graphics/samples/stacked_bar.py new file mode 100644 index 00000000000..9ba2a962f82 --- /dev/null +++ b/bin/reportlab/graphics/samples/stacked_bar.py @@ -0,0 +1,85 @@ +#Autogenerated by ReportLab guiedit do not edit +from reportlab.graphics.charts.legends import Legend +from reportlab.graphics.charts.barcharts import HorizontalBarChart +from reportlab.graphics.shapes import Drawing, _DrawingEditorMixin, String +from reportlab.graphics.charts.textlabels import Label +from excelcolors import * + +class StackedBar(_DrawingEditorMixin,Drawing): + def __init__(self,width=200,height=150,*args,**kw): + apply(Drawing.__init__,(self,width,height)+args,kw) + self._add(self,HorizontalBarChart(),name='chart',validate=None,desc="The main chart") + self.chart.width = 115 + self.chart.height = 80 + self.chart.x = 30 + self.chart.y = 40 + self.chart.bars[0].fillColor = color01 + self.chart.bars[1].fillColor = color02 + self.chart.bars[2].fillColor = color03 + self.chart.bars[3].fillColor = color04 + self.chart.bars[4].fillColor = color05 + self.chart.bars[5].fillColor = color06 + self.chart.bars[6].fillColor = color07 + self.chart.bars[7].fillColor = color08 + self.chart.bars[8].fillColor = color09 + self.chart.bars[9].fillColor = color10 + self.chart.fillColor = backgroundGrey + self.chart.barLabels.fontName = 'Helvetica' + self.chart.valueAxis.labels.fontName = 'Helvetica' + self.chart.valueAxis.labels.fontSize = 6 + self.chart.valueAxis.forceZero = 1 + self.chart.data = [(100, 150, 180), (125, 180, 200)] + self.chart.groupSpacing = 15 + self.chart.valueAxis.avoidBoundFrac = 1 + self.chart.valueAxis.gridEnd = 80 + self.chart.valueAxis.tickDown = 3 + self.chart.valueAxis.visibleGrid = 1 + self.chart.categoryAxis.categoryNames = ['North', 'South', 'Central'] + self.chart.categoryAxis.tickLeft = 3 + self.chart.categoryAxis.labels.fontName = 'Helvetica' + self.chart.categoryAxis.labels.fontSize = 6 + self.chart.categoryAxis.labels.dx = -3 + self._add(self,Label(),name='Title',validate=None,desc="The title at the top of the chart") + self.Title.fontName = 'Helvetica-Bold' + self.Title.fontSize = 7 + self.Title.x = 100 + self.Title.y = 135 + self.Title._text = 'Chart Title' + self.Title.maxWidth = 180 + self.Title.height = 20 + self.Title.textAnchor ='middle' + self._add(self,Legend(),name='Legend',validate=None,desc="The legend or key for the chart") + self.Legend.colorNamePairs = [(color01, 'Widgets'), (color02, 'Sprockets')] + self.Legend.fontName = 'Helvetica' + self.Legend.fontSize = 7 + self.Legend.x = 153 + self.Legend.y = 85 + self.Legend.dxTextSpace = 5 + self.Legend.dy = 5 + self.Legend.dx = 5 + self.Legend.deltay = 5 + self.Legend.alignment ='right' + self._add(self,Label(),name='XLabel',validate=None,desc="The label on the horizontal axis") + self.XLabel.fontName = 'Helvetica' + self.XLabel.fontSize = 7 + self.XLabel.x = 85 + self.XLabel.y = 10 + self.XLabel.textAnchor ='middle' + self.XLabel.maxWidth = 100 + self.XLabel.height = 20 + self.XLabel._text = "X Axis" + self._add(self,Label(),name='YLabel',validate=None,desc="The label on the vertical axis") + self.YLabel.fontName = 'Helvetica' + self.YLabel.fontSize = 7 + self.YLabel.x = 12 + self.YLabel.y = 80 + self.YLabel.angle = 90 + self.YLabel.textAnchor ='middle' + self.YLabel.maxWidth = 100 + self.YLabel.height = 20 + self.YLabel._text = "Y Axis" + self.chart.categoryAxis.style='stacked' + self._add(self,0,name='preview',validate=None,desc=None) + +if __name__=="__main__": #NORUNTESTS + StackedBar().save(formats=['pdf'],outDir=None,fnRoot='stacked_bar')
\ No newline at end of file diff --git a/bin/reportlab/graphics/samples/stacked_column.py b/bin/reportlab/graphics/samples/stacked_column.py new file mode 100644 index 00000000000..a50a1598c90 --- /dev/null +++ b/bin/reportlab/graphics/samples/stacked_column.py @@ -0,0 +1,84 @@ +#Autogenerated by ReportLab guiedit do not edit +from reportlab.graphics.charts.legends import Legend +from reportlab.graphics.charts.barcharts import VerticalBarChart +from reportlab.graphics.shapes import Drawing, _DrawingEditorMixin, String +from reportlab.graphics.charts.textlabels import Label +from excelcolors import * + +class StackedColumn(_DrawingEditorMixin,Drawing): + def __init__(self,width=200,height=150,*args,**kw): + apply(Drawing.__init__,(self,width,height)+args,kw) + self._add(self,VerticalBarChart(),name='chart',validate=None,desc="The main chart") + self.chart.width = 115 + self.chart.height = 80 + self.chart.x = 30 + self.chart.y = 40 + self.chart.bars[0].fillColor = color01 + self.chart.bars[1].fillColor = color02 + self.chart.bars[2].fillColor = color03 + self.chart.bars[3].fillColor = color04 + self.chart.bars[4].fillColor = color05 + self.chart.bars[5].fillColor = color06 + self.chart.bars[6].fillColor = color07 + self.chart.bars[7].fillColor = color08 + self.chart.bars[8].fillColor = color09 + self.chart.bars[9].fillColor = color10 + self.chart.fillColor = backgroundGrey + self.chart.barLabels.fontName = 'Helvetica' + self.chart.valueAxis.labels.fontName = 'Helvetica' + self.chart.valueAxis.labels.fontSize = 7 + self.chart.valueAxis.forceZero = 1 + self.chart.data = [(100, 150, 180), (125, 180, 200)] + self.chart.groupSpacing = 15 + self.chart.valueAxis.avoidBoundFrac = 1 + self.chart.valueAxis.gridEnd = 115 + self.chart.valueAxis.tickLeft = 3 + self.chart.valueAxis.visibleGrid = 1 + self.chart.categoryAxis.categoryNames = ['North', 'South', 'Central'] + self.chart.categoryAxis.tickDown = 3 + self.chart.categoryAxis.labels.fontName = 'Helvetica' + self.chart.categoryAxis.labels.fontSize = 7 + self._add(self,Label(),name='Title',validate=None,desc="The title at the top of the chart") + self.Title.fontName = 'Helvetica-Bold' + self.Title.fontSize = 7 + self.Title.x = 100 + self.Title.y = 135 + self.Title._text = 'Chart Title' + self.Title.maxWidth = 180 + self.Title.height = 20 + self.Title.textAnchor ='middle' + self._add(self,Legend(),name='Legend',validate=None,desc="The legend or key for the chart") + self.Legend.colorNamePairs = [(color01, 'Widgets'), (color02, 'Sprockets')] + self.Legend.fontName = 'Helvetica' + self.Legend.fontSize = 7 + self.Legend.x = 153 + self.Legend.y = 85 + self.Legend.dxTextSpace = 5 + self.Legend.dy = 5 + self.Legend.dx = 5 + self.Legend.deltay = 5 + self.Legend.alignment ='right' + self._add(self,Label(),name='XLabel',validate=None,desc="The label on the horizontal axis") + self.XLabel.fontName = 'Helvetica' + self.XLabel.fontSize = 7 + self.XLabel.x = 85 + self.XLabel.y = 10 + self.XLabel.textAnchor ='middle' + self.XLabel.maxWidth = 100 + self.XLabel.height = 20 + self.XLabel._text = "X Axis" + self._add(self,Label(),name='YLabel',validate=None,desc="The label on the vertical axis") + self.YLabel.fontName = 'Helvetica' + self.YLabel.fontSize = 7 + self.YLabel.x = 12 + self.YLabel.y = 80 + self.YLabel.angle = 90 + self.YLabel.textAnchor ='middle' + self.YLabel.maxWidth = 100 + self.YLabel.height = 20 + self.YLabel._text = "Y Axis" + self.chart.categoryAxis.style='stacked' + self._add(self,0,name='preview',validate=None,desc=None) + +if __name__=="__main__": #NORUNTESTS + StackedColumn().save(formats=['pdf'],outDir=None,fnRoot='stacked_column') diff --git a/bin/reportlab/graphics/shapes.py b/bin/reportlab/graphics/shapes.py new file mode 100644 index 00000000000..6944cf84fb0 --- /dev/null +++ b/bin/reportlab/graphics/shapes.py @@ -0,0 +1,1244 @@ +#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/shapes.py +""" +core of the graphics library - defines Drawing and Shapes +""" +__version__=''' $Id$ ''' + +import string, os, sys +from math import pi, cos, sin, tan +from types import FloatType, IntType, ListType, TupleType, StringType, InstanceType +from pprint import pprint + +from reportlab.platypus import Flowable +from reportlab.rl_config import shapeChecking, verbose +from reportlab.lib import logger +from reportlab.lib import colors +from reportlab.lib.validators import * +from reportlab.lib.attrmap import * +from reportlab.lib.utils import fp_str +from reportlab.pdfbase.pdfmetrics import stringWidth + +class NotImplementedError(Exception): + pass + +# two constants for filling rules +NON_ZERO_WINDING = 'Non-Zero Winding' +EVEN_ODD = 'Even-Odd' + +## these can be overridden at module level before you start +#creating shapes. So, if using a special color model, +#this provides support for the rendering mechanism. +#you can change defaults globally before you start +#making shapes; one use is to substitute another +#color model cleanly throughout the drawing. + +STATE_DEFAULTS = { # sensible defaults for all + 'transform': (1,0,0,1,0,0), + + # styles follow SVG naming + 'strokeColor': colors.black, + 'strokeWidth': 1, + 'strokeLineCap': 0, + 'strokeLineJoin': 0, + 'strokeMiterLimit' : 'TBA', # don't know yet so let bomb here + 'strokeDashArray': None, + 'strokeOpacity': 1.0, #100% + + 'fillColor': colors.black, #...or text will be invisible + #'fillRule': NON_ZERO_WINDING, - these can be done later + #'fillOpacity': 1.0, #100% - can be done later + + 'fontSize': 10, + 'fontName': 'Times-Roman', + 'textAnchor': 'start' # can be start, middle, end, inherited + } + + +#################################################################### +# math utilities. These could probably be moved into lib +# somewhere. +#################################################################### + +# constructors for matrices: +def nullTransform(): + return (1, 0, 0, 1, 0, 0) + +def translate(dx, dy): + return (1, 0, 0, 1, dx, dy) + +def scale(sx, sy): + return (sx, 0, 0, sy, 0, 0) + +def rotate(angle): + a = angle * pi/180 + return (cos(a), sin(a), -sin(a), cos(a), 0, 0) + +def skewX(angle): + a = angle * pi/180 + return (1, 0, tan(a), 1, 0, 0) + +def skewY(angle): + a = angle * pi/180 + return (1, tan(a), 0, 1, 0, 0) + +def mmult(A, B): + "A postmultiplied by B" + # I checked this RGB + # [a0 a2 a4] [b0 b2 b4] + # [a1 a3 a5] * [b1 b3 b5] + # [ 1 ] [ 1 ] + # + return (A[0]*B[0] + A[2]*B[1], + A[1]*B[0] + A[3]*B[1], + A[0]*B[2] + A[2]*B[3], + A[1]*B[2] + A[3]*B[3], + A[0]*B[4] + A[2]*B[5] + A[4], + A[1]*B[4] + A[3]*B[5] + A[5]) + +def inverse(A): + "For A affine 2D represented as 6vec return 6vec version of A**(-1)" + # I checked this RGB + det = float(A[0]*A[3] - A[2]*A[1]) + R = [A[3]/det, -A[1]/det, -A[2]/det, A[0]/det] + return tuple(R+[-R[0]*A[4]-R[2]*A[5],-R[1]*A[4]-R[3]*A[5]]) + +def zTransformPoint(A,v): + "Apply the homogenous part of atransformation a to vector v --> A*v" + return (A[0]*v[0]+A[2]*v[1],A[1]*v[0]+A[3]*v[1]) + +def transformPoint(A,v): + "Apply transformation a to vector v --> A*v" + return (A[0]*v[0]+A[2]*v[1]+A[4],A[1]*v[0]+A[3]*v[1]+A[5]) + +def transformPoints(matrix, V): + return map(transformPoint, V) + +def zTransformPoints(matrix, V): + return map(lambda x,matrix=matrix: zTransformPoint(matrix,x), V) + +def _textBoxLimits(text, font, fontSize, leading, textAnchor, boxAnchor): + w = 0 + for t in text: + w = max(w,stringWidth(t,font, fontSize)) + + h = len(text)*leading + yt = fontSize + if boxAnchor[0]=='s': + yb = -h + yt = yt - h + elif boxAnchor[0]=='n': + yb = 0 + else: + yb = -h/2.0 + yt = yt + yb + + if boxAnchor[-1]=='e': + xb = -w + if textAnchor=='end': xt = 0 + elif textAnchor=='start': xt = -w + else: xt = -w/2.0 + elif boxAnchor[-1]=='w': + xb = 0 + if textAnchor=='end': xt = w + elif textAnchor=='start': xt = 0 + else: xt = w/2.0 + else: + xb = -w/2.0 + if textAnchor=='end': xt = -xb + elif textAnchor=='start': xt = xb + else: xt = 0 + + return xb, yb, w, h, xt, yt + +def _rotatedBoxLimits( x, y, w, h, angle): + ''' + Find the corner points of the rotated w x h sized box at x,y + return the corner points and the min max points in the original space + ''' + C = zTransformPoints(rotate(angle),((x,y),(x+w,y),(x+w,y+h),(x,y+h))) + X = map(lambda x: x[0], C) + Y = map(lambda x: x[1], C) + return min(X), max(X), min(Y), max(Y), C + + +class _DrawTimeResizeable: + '''Addin class to provide the horribleness of _drawTimeResize''' + def _drawTimeResize(self,w,h): + if hasattr(self,'_canvas'): + canvas = self._canvas + drawing = canvas._drawing + drawing.width, drawing.height = w, h + if hasattr(canvas,'_drawTimeResize'): + canvas._drawTimeResize(w,h) + +class _SetKeyWordArgs: + def __init__(self, keywords={}): + """In general properties may be supplied to the constructor.""" + for key, value in keywords.items(): + setattr(self, key, value) + + +################################################################# +# +# Helper functions for working out bounds +# +################################################################# + +def getRectsBounds(rectList): + # filter out any None objects, e.g. empty groups + L = filter(lambda x: x is not None, rectList) + if not L: return None + + xMin, yMin, xMax, yMax = L[0] + for (x1, y1, x2, y2) in L[1:]: + if x1 < xMin: + xMin = x1 + if x2 > xMax: + xMax = x2 + if y1 < yMin: + yMin = y1 + if y2 > yMax: + yMax = y2 + return (xMin, yMin, xMax, yMax) + +def getPathBounds(points): + n = len(points) + f = lambda i,p = points: p[i] + xs = map(f,xrange(0,n,2)) + ys = map(f,xrange(1,n,2)) + return (min(xs), min(ys), max(xs), max(ys)) + +def getPointsBounds(pointList): + "Helper function for list of points" + first = pointList[0] + if type(first) in (ListType, TupleType): + xs = map(lambda xy: xy[0],pointList) + ys = map(lambda xy: xy[1],pointList) + return (min(xs), min(ys), max(xs), max(ys)) + else: + return getPathBounds(pointList) + +################################################################# +# +# And now the shapes themselves.... +# +################################################################# +class Shape(_SetKeyWordArgs,_DrawTimeResizeable): + """Base class for all nodes in the tree. Nodes are simply + packets of data to be created, stored, and ultimately + rendered - they don't do anything active. They provide + convenience methods for verification but do not + check attribiute assignments or use any clever setattr + tricks this time.""" + _attrMap = AttrMap() + + def copy(self): + """Return a clone of this shape.""" + + # implement this in the descendants as they need the right init methods. + raise NotImplementedError, "No copy method implemented for %s" % self.__class__.__name__ + + def getProperties(self,recur=1): + """Interface to make it easy to extract automatic + documentation""" + + #basic nodes have no children so this is easy. + #for more complex objects like widgets you + #may need to override this. + props = {} + for key, value in self.__dict__.items(): + if key[0:1] <> '_': + props[key] = value + return props + + def setProperties(self, props): + """Supports the bulk setting if properties from, + for example, a GUI application or a config file.""" + + self.__dict__.update(props) + #self.verify() + + def dumpProperties(self, prefix=""): + """Convenience. Lists them on standard output. You + may provide a prefix - mostly helps to generate code + samples for documentation.""" + + propList = self.getProperties().items() + propList.sort() + if prefix: + prefix = prefix + '.' + for (name, value) in propList: + print '%s%s = %s' % (prefix, name, value) + + def verify(self): + """If the programmer has provided the optional + _attrMap attribute, this checks all expected + attributes are present; no unwanted attributes + are present; and (if a checking function is found) + checks each attribute. Either succeeds or raises + an informative exception.""" + + if self._attrMap is not None: + for key in self.__dict__.keys(): + if key[0] <> '_': + assert self._attrMap.has_key(key), "Unexpected attribute %s found in %s" % (key, self) + for (attr, metavalue) in self._attrMap.items(): + assert hasattr(self, attr), "Missing attribute %s from %s" % (attr, self) + value = getattr(self, attr) + assert metavalue.validate(value), "Invalid value %s for attribute %s in class %s" % (value, attr, self.__class__.__name__) + + if shapeChecking: + """This adds the ability to check every attribute assignment as it is made. + It slows down shapes but is a big help when developing. It does not + get defined if rl_config.shapeChecking = 0""" + def __setattr__(self, attr, value): + """By default we verify. This could be off + in some parallel base classes.""" + validateSetattr(self,attr,value) #from reportlab.lib.attrmap + + def getBounds(self): + "Returns bounding rectangle of object as (x1,y1,x2,y2)" + raise NotImplementedError("Shapes and widgets must implement getBounds") + +class Group(Shape): + """Groups elements together. May apply a transform + to its contents. Has a publicly accessible property + 'contents' which may be used to iterate over contents. + In addition, child nodes may be given a name in which + case they are subsequently accessible as properties.""" + + _attrMap = AttrMap( + transform = AttrMapValue(isTransform,desc="Coordinate transformation to apply"), + contents = AttrMapValue(isListOfShapes,desc="Contained drawable elements"), + ) + + def __init__(self, *elements, **keywords): + """Initial lists of elements may be provided to allow + compact definitions in literal Python code. May or + may not be useful.""" + + # Groups need _attrMap to be an instance rather than + # a class attribute, as it may be extended at run time. + self._attrMap = self._attrMap.clone() + self.contents = [] + self.transform = (1,0,0,1,0,0) + for elt in elements: + self.add(elt) + # this just applies keywords; do it at the end so they + #don;t get overwritten + _SetKeyWordArgs.__init__(self, keywords) + + def _addNamedNode(self,name,node): + 'if name is not None add an attribute pointing to node and add to the attrMap' + if name: + if name not in self._attrMap.keys(): + self._attrMap[name] = AttrMapValue(isValidChild) + setattr(self, name, node) + + def add(self, node, name=None): + """Appends non-None child node to the 'contents' attribute. In addition, + if a name is provided, it is subsequently accessible by name + """ + # propagates properties down + if node is not None: + assert isValidChild(node), "Can only add Shape or UserNode objects to a Group" + self.contents.append(node) + self._addNamedNode(name,node) + + def _nn(self,node): + self.add(node) + return self.contents[-1] + + def insert(self, i, n, name=None): + 'Inserts sub-node n in contents at specified location' + if n is not None: + assert isValidChild(n), "Can only insert Shape or UserNode objects in a Group" + if i<0: + self.contents[i:i] =[n] + else: + self.contents.insert(i,n) + self._addNamedNode(name,n) + + def expandUserNodes(self): + """Return a new object which only contains primitive shapes.""" + + # many limitations - shared nodes become multiple ones, + obj = isinstance(self,Drawing) and Drawing(self.width,self.height) or Group() + obj._attrMap = self._attrMap.clone() + if hasattr(obj,'transform'): obj.transform = self.transform[:] + + self_contents = self.contents + a = obj.contents.append + for child in self_contents: + if isinstance(child, UserNode): + newChild = child.provideNode() + elif isinstance(child, Group): + newChild = child.expandUserNodes() + else: + newChild = child.copy() + a(newChild) + + self._copyNamedContents(obj) + return obj + + def _explode(self): + ''' return a fully expanded object''' + from reportlab.graphics.widgetbase import Widget + obj = Group() + if hasattr(obj,'transform'): obj.transform = self.transform[:] + P = self.contents[:] # pending nodes + while P: + n = P.pop(0) + if isinstance(n, UserNode): + P.append(n.provideNode()) + elif isinstance(n, Group): + n = n._explode() + if n.transform==(1,0,0,1,0,0): + obj.contents.extend(n.contents) + else: + obj.add(n) + else: + obj.add(n) + return obj + + def _copyContents(self,obj): + for child in self.contents: + obj.contents.append(child) + + def _copyNamedContents(self,obj,aKeys=None,noCopy=('contents',)): + from copy import copy + self_contents = self.contents + if not aKeys: aKeys = self._attrMap.keys() + for (k, v) in self.__dict__.items(): + if v in self_contents: + pos = self_contents.index(v) + setattr(obj, k, obj.contents[pos]) + elif k in aKeys and k not in noCopy: + setattr(obj, k, copy(v)) + + def _copy(self,obj): + """copies to obj""" + obj._attrMap = self._attrMap.clone() + self._copyContents(obj) + self._copyNamedContents(obj) + return obj + + def copy(self): + """returns a copy""" + return self._copy(self.__class__()) + + def rotate(self, theta): + """Convenience to help you set transforms""" + self.transform = mmult(self.transform, rotate(theta)) + + def translate(self, dx, dy): + """Convenience to help you set transforms""" + self.transform = mmult(self.transform, translate(dx, dy)) + + def scale(self, sx, sy): + """Convenience to help you set transforms""" + self.transform = mmult(self.transform, scale(sx, sy)) + + + def skew(self, kx, ky): + """Convenience to help you set transforms""" + self.transform = mmult(mmult(self.transform, skewX(kx)),skewY(ky)) + + def shift(self, x, y): + '''Convenience function to set the origin arbitrarily''' + self.transform = self.transform[:-2]+(x,y) + + def asDrawing(self, width, height): + """ Convenience function to make a drawing from a group + After calling this the instance will be a drawing! + """ + self.__class__ = Drawing + self._attrMap.update(self._xtraAttrMap) + self.width = width + self.height = height + + def getContents(self): + '''Return the list of things to be rendered + override to get more complicated behaviour''' + b = getattr(self,'background',None) + C = self.contents + if b and b not in C: C = [b]+C + return C + + def getBounds(self): + if self.contents: + b = [] + for elem in self.contents: + b.append(elem.getBounds()) + x1 = getRectsBounds(b) + if x1 is None: return None + x1, y1, x2, y2 = x1 + trans = self.transform + corners = [[x1,y1], [x1, y2], [x2, y1], [x2,y2]] + newCorners = [] + for corner in corners: + newCorners.append(transformPoint(trans, corner)) + return getPointsBounds(newCorners) + else: + #empty group needs a sane default; this + #will happen when interactively creating a group + #nothing has been added to yet. The alternative is + #to handle None as an allowed return value everywhere. + return None + +def _addObjImport(obj,I,n=None): + '''add an import of obj's class to a dictionary of imports''' #' + from inspect import getmodule + c = obj.__class__ + m = getmodule(c).__name__ + n = n or c.__name__ + if not I.has_key(m): + I[m] = [n] + elif n not in I[m]: + I[m].append(n) + +def _repr(self,I=None): + '''return a repr style string with named fixed args first, then keywords''' + if type(self) is InstanceType: + if self is EmptyClipPath: + _addObjImport(self,I,'EmptyClipPath') + return 'EmptyClipPath' + if I: _addObjImport(self,I) + if isinstance(self,Shape): + from inspect import getargs + args, varargs, varkw = getargs(self.__init__.im_func.func_code) + P = self.getProperties() + s = self.__class__.__name__+'(' + for n in args[1:]: + v = P[n] + del P[n] + s = s + '%s,' % _repr(v,I) + for n,v in P.items(): + v = P[n] + s = s + '%s=%s,' % (n, _repr(v,I)) + return s[:-1]+')' + else: + return repr(self) + elif type(self) is FloatType: + return fp_str(self) + elif type(self) in (ListType,TupleType): + s = '' + for v in self: + s = s + '%s,' % _repr(v,I) + if type(self) is ListType: + return '[%s]' % s[:-1] + else: + return '(%s%s)' % (s[:-1],len(self)==1 and ',' or '') + else: + return repr(self) + +def _renderGroupPy(G,pfx,I,i=0,indent='\t\t'): + s = '' + C = getattr(G,'transform',None) + if C: s = s + ('%s%s.transform = %s\n' % (indent,pfx,_repr(C))) + C = G.contents + for n in C: + if isinstance(n, Group): + npfx = 'v%d' % i + i = i + 1 + s = s + '%s%s=%s._nn(Group())\n' % (indent,npfx,pfx) + s = s + _renderGroupPy(n,npfx,I,i,indent) + i = i - 1 + else: + s = s + '%s%s.add(%s)\n' % (indent,pfx,_repr(n,I)) + return s + +class Drawing(Group, Flowable): + """Outermost container; the thing a renderer works on. + This has no properties except a height, width and list + of contents.""" + + _xtraAttrMap = AttrMap( + width = AttrMapValue(isNumber,desc="Drawing width in points."), + height = AttrMapValue(isNumber,desc="Drawing height in points."), + canv = AttrMapValue(None), + background = AttrMapValue(isValidChildOrNone,desc="Background widget for the drawing"), + hAlign = AttrMapValue(OneOf("LEFT", "RIGHT", "CENTER", "CENTRE"), desc="Alignment within parent document"), + vAlign = AttrMapValue(OneOf("TOP", "BOTTOM", "CENTER", "CENTRE"), desc="Alignment within parent document") + ) + + _attrMap = AttrMap(BASE=Group) + _attrMap.update(_xtraAttrMap) + + def __init__(self, width=400, height=200, *nodes, **keywords): + self.background = None + apply(Group.__init__,(self,)+nodes,keywords) + self.width = width + self.height = height + self.hAlign = 'LEFT' + self.vAlign = 'BOTTOM' + + def _renderPy(self): + I = {'reportlab.graphics.shapes': ['_DrawingEditorMixin','Drawing','Group']} + G = _renderGroupPy(self._explode(),'self',I) + n = 'ExplodedDrawing_' + self.__class__.__name__ + s = '#Autogenerated by ReportLab guiedit do not edit\n' + for m, o in I.items(): + s = s + 'from %s import %s\n' % (m,string.replace(str(o)[1:-1],"'","")) + s = s + '\nclass %s(_DrawingEditorMixin,Drawing):\n' % n + s = s + '\tdef __init__(self,width=%s,height=%s,*args,**kw):\n' % (self.width,self.height) + s = s + '\t\tapply(Drawing.__init__,(self,width,height)+args,kw)\n' + s = s + G + s = s + '\n\nif __name__=="__main__": #NORUNTESTS\n\t%s().save(formats=[\'pdf\'],outDir=\'.\',fnRoot=None)\n' % n + return s + + def draw(self): + """This is used by the Platypus framework to let the document + draw itself in a story. It is specific to PDF and should not + be used directly.""" + + import renderPDF + R = renderPDF._PDFRenderer() + R.draw(self, self.canv, 0, 0) + + def expandUserNodes(self): + """Return a new drawing which only contains primitive shapes.""" + obj = Group.expandUserNodes(self) + obj.width = self.width + obj.height = self.height + return obj + + def copy(self,obj): + """Returns a copy""" + return self._copy(Drawing(self.width, self.height)) + + def asGroup(self,*args,**kw): + return self._copy(apply(Group,args,kw)) + + def save(self, formats=None, verbose=None, fnRoot=None, outDir=None, title=''): + """Saves copies of self in desired location and formats. + + Multiple formats can be supported in one call""" + from reportlab import rl_config + ext = '' + if not fnRoot: + fnRoot = getattr(self,'fileNamePattern',(self.__class__.__name__+'%03d')) + chartId = getattr(self,'chartId',0) + if callable(fnRoot): + fnRoot = fnRoot(chartId) + else: + try: + fnRoot = fnRoot % getattr(self,'chartId',0) + except TypeError, err: + if str(err) != 'not all arguments converted': raise + + if os.path.isabs(fnRoot): + outDir, fnRoot = os.path.split(fnRoot) + else: + outDir = outDir or getattr(self,'outDir','.') + if not os.path.isabs(outDir): outDir = os.path.join(os.path.dirname(sys.argv[0]),outDir) + if not os.path.isdir(outDir): os.makedirs(outDir) + fnroot = os.path.normpath(os.path.join(outDir,fnRoot)) + plotMode = os.path.splitext(fnroot) + if string.lower(plotMode[1][1:]) in ['pdf','ps','eps','gif','png','jpg','jpeg','pct','pict','tiff','tif','py','bmp']: + fnroot = plotMode[0] + + plotMode, verbose = formats or getattr(self,'formats',['pdf']), (verbose is not None and (verbose,) or (getattr(self,'verbose',verbose),))[0] + _saved = logger.warnOnce.enabled, logger.infoOnce.enabled + logger.warnOnce.enabled = logger.infoOnce.enabled = verbose + if 'pdf' in plotMode: + from reportlab.graphics import renderPDF + filename = fnroot+'.pdf' + if verbose: print "generating PDF file %s" % filename + renderPDF.drawToFile(self, filename, title, showBoundary=getattr(self,'showBorder',rl_config.showBoundary)) + ext = ext + '/.pdf' + if sys.platform=='mac': + import macfs, macostools + macfs.FSSpec(filename).SetCreatorType("CARO", "PDF ") + macostools.touched(filename) + + for bmFmt in ['gif','png','tif','jpg','tiff','pct','pict', 'bmp']: + if bmFmt in plotMode: + from reportlab.graphics import renderPM + filename = '%s.%s' % (fnroot,bmFmt) + if verbose: print "generating %s file %s" % (bmFmt,filename) + renderPM.drawToFile(self, filename,fmt=bmFmt,showBoundary=getattr(self,'showBorder',rl_config.showBoundary)) + ext = ext + '/.' + bmFmt + + if 'eps' in plotMode: + from rlextra.graphics import renderPS_SEP + filename = fnroot+'.eps' + if verbose: print "generating EPS file %s" % filename + renderPS_SEP.drawToFile(self, + filename, + title = fnroot, + dept = getattr(self,'EPS_info',['Testing'])[0], + company = getattr(self,'EPS_info',['','ReportLab'])[1], + preview = getattr(self,'preview',1), + showBoundary=getattr(self,'showBorder',rl_config.showBoundary)) + ext = ext + '/.eps' + + if 'ps' in plotMode: + from reportlab.graphics import renderPS + filename = fnroot+'.ps' + if verbose: print "generating EPS file %s" % filename + renderPS.drawToFile(self, filename, showBoundary=getattr(self,'showBorder',rl_config.showBoundary)) + ext = ext + '/.ps' + + if 'py' in plotMode: + filename = fnroot+'.py' + if verbose: print "generating py file %s" % filename + open(filename,'w').write(self._renderPy()) + ext = ext + '/.py' + + logger.warnOnce.enabled, logger.infoOnce.enabled = _saved + if hasattr(self,'saveLogger'): + self.saveLogger(fnroot,ext) + return ext and fnroot+ext[1:] or '' + + + def asString(self, format, verbose=None, preview=0): + """Converts to an 8 bit string in given format.""" + assert format in ['pdf','ps','eps','gif','png','jpg','jpeg','bmp','ppm','tiff','tif','py','pict','pct'], 'Unknown file format "%s"' % format + from reportlab import rl_config + #verbose = verbose is not None and (verbose,) or (getattr(self,'verbose',verbose),)[0] + if format == 'pdf': + from reportlab.graphics import renderPDF + return renderPDF.drawToString(self) + elif format in ['gif','png','tif','jpg','pct','pict','bmp','ppm']: + from reportlab.graphics import renderPM + return renderPM.drawToString(self, fmt=format) + elif format == 'eps': + from rlextra.graphics import renderPS_SEP + return renderPS_SEP.drawToString(self, + preview = preview, + showBoundary=getattr(self,'showBorder',rl_config.showBoundary)) + elif format == 'ps': + from reportlab.graphics import renderPS + return renderPS.drawToString(self, showBoundary=getattr(self,'showBorder',rl_config.showBoundary)) + elif format == 'py': + return self._renderPy() + +class _DrawingEditorMixin: + '''This is a mixin to provide functionality for edited drawings''' + def _add(self,obj,value,name=None,validate=None,desc=None,pos=None): + ''' + effectively setattr(obj,name,value), but takes care of things with _attrMaps etc + ''' + ivc = isValidChild(value) + if name and hasattr(obj,'_attrMap'): + if not obj.__dict__.has_key('_attrMap'): + obj._attrMap = obj._attrMap.clone() + if ivc and validate is None: validate = isValidChild + obj._attrMap[name] = AttrMapValue(validate,desc) + if hasattr(obj,'add') and ivc: + if pos: + obj.insert(pos,value,name) + else: + obj.add(value,name) + elif name: + setattr(obj,name,value) + else: + raise ValueError, "Can't add, need name" + +class LineShape(Shape): + # base for types of lines + + _attrMap = AttrMap( + strokeColor = AttrMapValue(isColorOrNone), + strokeWidth = AttrMapValue(isNumber), + strokeLineCap = AttrMapValue(None), + strokeLineJoin = AttrMapValue(None), + strokeMiterLimit = AttrMapValue(isNumber), + strokeDashArray = AttrMapValue(isListOfNumbersOrNone), + ) + + def __init__(self, kw): + self.strokeColor = STATE_DEFAULTS['strokeColor'] + self.strokeWidth = 1 + self.strokeLineCap = 0 + self.strokeLineJoin = 0 + self.strokeMiterLimit = 0 + self.strokeDashArray = None + self.setProperties(kw) + + +class Line(LineShape): + _attrMap = AttrMap(BASE=LineShape, + x1 = AttrMapValue(isNumber), + y1 = AttrMapValue(isNumber), + x2 = AttrMapValue(isNumber), + y2 = AttrMapValue(isNumber), + ) + + def __init__(self, x1, y1, x2, y2, **kw): + LineShape.__init__(self, kw) + self.x1 = x1 + self.y1 = y1 + self.x2 = x2 + self.y2 = y2 + + def getBounds(self): + "Returns bounding rectangle of object as (x1,y1,x2,y2)" + return (self.x1, self.y1, self.x2, self.y2) + + +class SolidShape(LineShape): + # base for anything with outline and content + + _attrMap = AttrMap(BASE=LineShape, + fillColor = AttrMapValue(isColorOrNone), + ) + + def __init__(self, kw): + self.fillColor = STATE_DEFAULTS['fillColor'] + # do this at the end so keywords overwrite + #the above settings + LineShape.__init__(self, kw) + + +# path operator constants +_MOVETO, _LINETO, _CURVETO, _CLOSEPATH = range(4) +_PATH_OP_ARG_COUNT = (2, 2, 6, 0) # [moveTo, lineTo, curveTo, closePath] +_PATH_OP_NAMES=['moveTo','lineTo','curveTo','closePath'] + +def _renderPath(path, drawFuncs): + """Helper function for renderers.""" + # this could be a method of Path... + points = path.points + i = 0 + hadClosePath = 0 + hadMoveTo = 0 + for op in path.operators: + nArgs = _PATH_OP_ARG_COUNT[op] + func = drawFuncs[op] + j = i + nArgs + apply(func, points[i:j]) + i = j + if op == _CLOSEPATH: + hadClosePath = hadClosePath + 1 + if op == _MOVETO: + hadMoveTo = hadMoveTo + 1 + return hadMoveTo == hadClosePath + +class Path(SolidShape): + """Path, made up of straight lines and bezier curves.""" + + _attrMap = AttrMap(BASE=SolidShape, + points = AttrMapValue(isListOfNumbers), + operators = AttrMapValue(isListOfNumbers), + isClipPath = AttrMapValue(isBoolean), + ) + + def __init__(self, points=None, operators=None, isClipPath=0, **kw): + SolidShape.__init__(self, kw) + if points is None: + points = [] + if operators is None: + operators = [] + assert len(points) % 2 == 0, 'Point list must have even number of elements!' + self.points = points + self.operators = operators + self.isClipPath = isClipPath + + def copy(self): + new = Path(self.points[:], self.operators[:]) + new.setProperties(self.getProperties()) + return new + + def moveTo(self, x, y): + self.points.extend([x, y]) + self.operators.append(_MOVETO) + + def lineTo(self, x, y): + self.points.extend([x, y]) + self.operators.append(_LINETO) + + def curveTo(self, x1, y1, x2, y2, x3, y3): + self.points.extend([x1, y1, x2, y2, x3, y3]) + self.operators.append(_CURVETO) + + def closePath(self): + self.operators.append(_CLOSEPATH) + + def getBounds(self): + return getPathBounds(self.points) + +EmptyClipPath=Path() #special path + +def getArcPoints(centerx, centery, radius, startangledegrees, endangledegrees, yradius=None, degreedelta=None, reverse=None): + degreedelta = degreedelta or 1 + if yradius is None: yradius = radius + points = [] + from math import sin, cos, pi + degreestoradians = pi/180.0 + radiansdelta = degreedelta*degreestoradians + startangle = startangledegrees*degreestoradians + endangle = endangledegrees*degreestoradians + while endangle<startangle: + endangle = endangle+2*pi + angle = float(endangle - startangle) + if angle>.001: + n = max(int(angle/radiansdelta+0.5),1) + radiansdelta = angle/n + a = points.append + for angle in xrange(n+1): + angle = startangle+angle*radiansdelta + a((centerx+radius*cos(angle),centery+yradius*sin(angle))) + #a((centerx+radius*cos(endangle),centery+yradius*sin(endangle))) + if reverse: points.reverse() + return points + +class ArcPath(Path): + '''Path with an addArc method''' + def addArc(self, centerx, centery, radius, startangledegrees, endangledegrees, yradius=None, degreedelta=None, moveTo=None, reverse=None): + P = getArcPoints(centerx, centery, radius, startangledegrees, endangledegrees, yradius=yradius, degreedelta=degreedelta, reverse=reverse) + if moveTo or not len(self.operators): + self.moveTo(P[0][0],P[0][1]) + del P[0] + for x, y in P: self.lineTo(x,y) + +def definePath(pathSegs=[],isClipPath=0, dx=0, dy=0, **kw): + O = [] + P = [] + for seg in pathSegs: + if type(seg) not in [ListType,TupleType]: + opName = seg + args = [] + else: + opName = seg[0] + args = seg[1:] + if opName not in _PATH_OP_NAMES: + raise ValueError, 'bad operator name %s' % opName + op = _PATH_OP_NAMES.index(opName) + if len(args)!=_PATH_OP_ARG_COUNT[op]: + raise ValueError, '%s bad arguments %s' % (opName,str(args)) + O.append(op) + P.extend(list(args)) + for d,o in (dx,0), (dy,1): + for i in xrange(o,len(P),2): + P[i] = P[i]+d + return apply(Path,(P,O,isClipPath),kw) + +class Rect(SolidShape): + """Rectangle, possibly with rounded corners.""" + + _attrMap = AttrMap(BASE=SolidShape, + x = AttrMapValue(isNumber), + y = AttrMapValue(isNumber), + width = AttrMapValue(isNumber), + height = AttrMapValue(isNumber), + rx = AttrMapValue(isNumber), + ry = AttrMapValue(isNumber), + ) + + def __init__(self, x, y, width, height, rx=0, ry=0, **kw): + SolidShape.__init__(self, kw) + self.x = x + self.y = y + self.width = width + self.height = height + self.rx = rx + self.ry = ry + + def copy(self): + new = Rect(self.x, self.y, self.width, self.height) + new.setProperties(self.getProperties()) + return new + + def getBounds(self): + return (self.x, self.y, self.x + self.width, self.y + self.height) + + +class Image(SolidShape): + """Bitmap image.""" + + _attrMap = AttrMap(BASE=SolidShape, + x = AttrMapValue(isNumber), + y = AttrMapValue(isNumber), + width = AttrMapValue(isNumberOrNone), + height = AttrMapValue(isNumberOrNone), + path = AttrMapValue(None), + ) + + def __init__(self, x, y, width, height, path, **kw): + SolidShape.__init__(self, kw) + self.x = x + self.y = y + self.width = width + self.height = height + self.path = path + + def copy(self): + new = Image(self.x, self.y, self.width, self.height, self.path) + new.setProperties(self.getProperties()) + return new + + def getBounds(self): + return (self.x, self.y, self.x + width, self.y + width) + +class Circle(SolidShape): + + _attrMap = AttrMap(BASE=SolidShape, + cx = AttrMapValue(isNumber), + cy = AttrMapValue(isNumber), + r = AttrMapValue(isNumber), + ) + + def __init__(self, cx, cy, r, **kw): + SolidShape.__init__(self, kw) + self.cx = cx + self.cy = cy + self.r = r + + def copy(self): + new = Circle(self.cx, self.cy, self.r) + new.setProperties(self.getProperties()) + return new + + def getBounds(self): + return (self.cx - self.r, self.cy - self.r, self.cx + self.r, self.cy + self.r) + +class Ellipse(SolidShape): + + _attrMap = AttrMap(BASE=SolidShape, + cx = AttrMapValue(isNumber), + cy = AttrMapValue(isNumber), + rx = AttrMapValue(isNumber), + ry = AttrMapValue(isNumber), + ) + + def __init__(self, cx, cy, rx, ry, **kw): + SolidShape.__init__(self, kw) + self.cx = cx + self.cy = cy + self.rx = rx + self.ry = ry + + def copy(self): + new = Ellipse(self.cx, self.cy, self.rx, self.ry) + new.setProperties(self.getProperties()) + return new + + def getBounds(self): + return (self.cx - self.rx, self.cy - self.ry, self.cx + self.rx, self.cy + self.ry) + +class Wedge(SolidShape): + """A "slice of a pie" by default translates to a polygon moves anticlockwise + from start angle to end angle""" + + _attrMap = AttrMap(BASE=SolidShape, + centerx = AttrMapValue(isNumber), + centery = AttrMapValue(isNumber), + radius = AttrMapValue(isNumber), + startangledegrees = AttrMapValue(isNumber), + endangledegrees = AttrMapValue(isNumber), + yradius = AttrMapValue(isNumberOrNone), + radius1 = AttrMapValue(isNumberOrNone), + yradius1 = AttrMapValue(isNumberOrNone), + ) + + degreedelta = 1 # jump every 1 degrees + + def __init__(self, centerx, centery, radius, startangledegrees, endangledegrees, yradius=None, **kw): + SolidShape.__init__(self, kw) + while endangledegrees<startangledegrees: + endangledegrees = endangledegrees+360 + #print "__init__" + self.centerx, self.centery, self.radius, self.startangledegrees, self.endangledegrees = \ + centerx, centery, radius, startangledegrees, endangledegrees + self.yradius = yradius + + def _xtraRadii(self): + yradius = getattr(self, 'yradius', None) + if yradius is None: yradius = self.radius + radius1 = getattr(self,'radius1', None) + yradius1 = getattr(self,'yradius1',radius1) + if radius1 is None: radius1 = yradius1 + return yradius, radius1, yradius1 + + #def __repr__(self): + # return "Wedge"+repr((self.centerx, self.centery, self.radius, self.startangledegrees, self.endangledegrees )) + #__str__ = __repr__ + + def asPolygon(self): + #print "asPolygon" + centerx= self.centerx + centery = self.centery + radius = self.radius + yradius, radius1, yradius1 = self._xtraRadii() + startangledegrees = self.startangledegrees + endangledegrees = self.endangledegrees + degreedelta = self.degreedelta + from math import sin, cos, pi + degreestoradians = pi/180.0 + radiansdelta = degreedelta*degreestoradians + startangle = startangledegrees*degreestoradians + endangle = endangledegrees*degreestoradians + while endangle<startangle: + endangle = endangle+2*pi + angle = float(endangle-startangle) + points = [] + if angle>.001: + n = max(1,int(angle/radiansdelta+0.5)) + radiansdelta = angle/n + a = points.append + CA = [] + CAA = CA.append + for angle in xrange(n+1): + angle = startangle+angle*radiansdelta + CAA((cos(angle),sin(angle))) + #CAA((cos(endangle),sin(endangle))) + for c,s in CA: + a(centerx+radius*c) + a(centery+yradius*s) + if (radius1==0 or radius1 is None) and (yradius1==0 or yradius1 is None): + a(centerx); a(centery) + else: + CA.reverse() + for c,s in CA: + a(centerx+radius1*c) + a(centery+yradius1*s) + return Polygon(points) + + def copy(self): + new = Wedge(self.centerx, + self.centery, + self.radius, + self.startangledegrees, + self.endangledegrees) + new.setProperties(self.getProperties()) + return new + + def getBounds(self): + return self.asPolygon().getBounds() + +class Polygon(SolidShape): + """Defines a closed shape; Is implicitly + joined back to the start for you.""" + + _attrMap = AttrMap(BASE=SolidShape, + points = AttrMapValue(isListOfNumbers), + ) + + def __init__(self, points=[], **kw): + SolidShape.__init__(self, kw) + assert len(points) % 2 == 0, 'Point list must have even number of elements!' + self.points = points + + def copy(self): + new = Polygon(self.points) + new.setProperties(self.getProperties()) + return new + + def getBounds(self): + return getPointsBounds(self.points) + +class PolyLine(LineShape): + """Series of line segments. Does not define a + closed shape; never filled even if apparently joined. + Put the numbers in the list, not two-tuples.""" + + _attrMap = AttrMap(BASE=LineShape, + points = AttrMapValue(isListOfNumbers), + ) + + def __init__(self, points=[], **kw): + LineShape.__init__(self, kw) + lenPoints = len(points) + if lenPoints: + if type(points[0]) in (ListType,TupleType): + L = [] + for (x,y) in points: + L.append(x) + L.append(y) + points = L + else: + assert len(points) % 2 == 0, 'Point list must have even number of elements!' + self.points = points + + def copy(self): + new = PolyLine(self.points) + new.setProperties(self.getProperties()) + return new + + def getBounds(self): + return getPointsBounds(self.points) + +class String(Shape): + """Not checked against the spec, just a way to make something work. + Can be anchored left, middle or end.""" + + # to do. + _attrMap = AttrMap( + x = AttrMapValue(isNumber), + y = AttrMapValue(isNumber), + text = AttrMapValue(isString), + fontName = AttrMapValue(None), + fontSize = AttrMapValue(isNumber), + fillColor = AttrMapValue(isColorOrNone), + textAnchor = AttrMapValue(isTextAnchor), + ) + + def __init__(self, x, y, text, **kw): + self.x = x + self.y = y + self.text = text + self.textAnchor = 'start' + self.fontName = STATE_DEFAULTS['fontName'] + self.fontSize = STATE_DEFAULTS['fontSize'] + self.fillColor = STATE_DEFAULTS['fillColor'] + self.setProperties(kw) + + def getEast(self): + return self.x + stringWidth(self.text,self.fontName,self.fontSize) + + def copy(self): + new = String(self.x, self.y, self.text) + new.setProperties(self.getProperties()) + return new + + def getBounds(self): + # assumes constant drop of 0.2*size to baseline + w = stringWidth(self.text,self.fontName,self.fontSize) + if self.textAnchor == 'start': + x = self.x + elif self.textAnchor == 'middle': + x = self.x - 0.5*w + elif self.textAnchor == 'end': + x = self.x - w + return (x, self.y - 0.2 * self.fontSize, x+w, self.y + self.fontSize) + +class UserNode(_DrawTimeResizeable): + """A simple template for creating a new node. The user (Python + programmer) may subclasses this. provideNode() must be defined to + provide a Shape primitive when called by a renderer. It does + NOT inherit from Shape, as the renderer always replaces it, and + your own classes can safely inherit from it without getting + lots of unintended behaviour.""" + + def provideNode(self): + """Override this to create your own node. This lets widgets be + added to drawings; they must create a shape (typically a group) + so that the renderer can draw the custom node.""" + + raise NotImplementedError, "this method must be redefined by the user/programmer" + + +def test(): + r = Rect(10,10,200,50) + import pprint + pp = pprint.pprint + print 'a Rectangle:' + pp(r.getProperties()) + print + print 'verifying...', + r.verify() + print 'OK' + #print 'setting rect.z = "spam"' + #r.z = 'spam' + print 'deleting rect.width' + del r.width + print 'verifying...', + r.verify() + + +if __name__=='__main__': + test() diff --git a/bin/reportlab/graphics/testdrawings.py b/bin/reportlab/graphics/testdrawings.py new file mode 100755 index 00000000000..b5048d1287c --- /dev/null +++ b/bin/reportlab/graphics/testdrawings.py @@ -0,0 +1,294 @@ +#! /usr/bin/python2.3 +#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/testdrawings.py +__version__=''' $Id $ ''' +"""This contains a number of routines to generate test drawings +for reportlab/graphics. For now they are contrived, but we will expand them +to try and trip up any parser. Feel free to add more. + +""" + +from reportlab.graphics.shapes import * +from reportlab.lib import colors + +def getDrawing1(): + """Hello World, on a rectangular background""" + + D = Drawing(400, 200) + D.add(Rect(50, 50, 300, 100, fillColor=colors.yellow)) #round corners + D.add(String(180,100, 'Hello World', fillColor=colors.red)) + + + return D + + +def getDrawing2(): + """This demonstrates the basic shapes. There are + no groups or references. Each solid shape should have + a purple fill.""" + D = Drawing(400, 200) #, fillColor=colors.purple) + + D.add(Line(10,10,390,190)) + D.add(Circle(100,100,20, fillColor=colors.purple)) + D.add(Circle(200,100,20, fillColor=colors.purple)) + D.add(Circle(300,100,20, fillColor=colors.purple)) + + D.add(Wedge(330,100,40, -10,40, fillColor=colors.purple)) + + D.add(PolyLine([120,10,130,20,140,10,150,20,160,10, + 170,20,180,10,190,20,200,10])) + + D.add(Polygon([300,20,350,20,390,80,300,75, 330, 40])) + + D.add(Ellipse(50, 150, 40, 20)) + + D.add(Rect(120, 150, 60, 30, + strokeWidth=10, + strokeColor=colors.red, + fillColor=colors.yellow)) #square corners + + D.add(Rect(220, 150, 60, 30, 10, 10)) #round corners + + D.add(String(10,50, 'Basic Shapes', fillColor=colors.black)) + + return D + + +##def getDrawing2(): +## """This drawing uses groups. Each group has two circles and a comment. +## The line style is set at group level and should be red for the left, +## bvlue for the right.""" +## D = Drawing(400, 200) +## +## Group1 = Group() +## +## Group1.add(String(50, 50, 'Group 1', fillColor=colors.black)) +## Group1.add(Circle(75,100,25)) +## Group1.add(Circle(125,100,25)) +## D.add(Group1) +## +## Group2 = Group( +## String(250, 50, 'Group 2', fillColor=colors.black), +## Circle(275,100,25), +## Circle(325,100,25)#, + + +##def getDrawing2(): +## """This drawing uses groups. Each group has two circles and a comment. +## The line style is set at group level and should be red for the left, +## bvlue for the right.""" +## D = Drawing(400, 200) +## +## Group1 = Group() +## +## Group1.add(String(50, 50, 'Group 1', fillColor=colors.black)) +## Group1.add(Circle(75,100,25)) +## Group1.add(Circle(125,100,25)) +## D.add(Group1) +## +## Group2 = Group( +## String(250, 50, 'Group 2', fillColor=colors.black), +## Circle(275,100,25), +## Circle(325,100,25)#, +## +## #group attributes +## #strokeColor=colors.blue +## ) +## D.add(Group2) + +## return D +## +## +##def getDrawing3(): +## """This uses a named reference object. The house is a 'subroutine' +## the basic brick colored walls are defined, but the roof and window +## color are undefined and may be set by the container.""" +## +## D = Drawing(400, 200, fill=colors.bisque) +## +## +## House = Group( +## Rect(2,20,36,30, fill=colors.bisque), #walls +## Polygon([0,20,40,20,20,5]), #roof +## Rect(8, 38, 8, 12), #door +## Rect(25, 38, 8, 7), #window +## Rect(8, 25, 8, 7), #window +## Rect(25, 25, 8, 7) #window +## +## ) +## D.addDef('MyHouse', House) +## +## # one row all the same color +## D.add(String(20, 40, 'British Street...',fill=colors.black)) +## for i in range(6): +## x = i * 50 +## D.add(NamedReference('MyHouse', +## House, +## transform=translate(x, 40), +## fill = colors.brown +## ) +## ) +## +## # now do a row all different +## D.add(String(20, 120, 'Mediterranean Street...',fill=colors.black)) +## x = 0 +## for color in (colors.blue, colors.yellow, colors.orange, +## colors.red, colors.green, colors.chartreuse): +## D.add(NamedReference('MyHouse', +## House, +## transform=translate(x,120), +## fill = color, +## ) +## ) +## x = x + 50 +## #..by popular demand, the mayor gets a big one at the end +## D.add(NamedReference('MyHouse', +## House, +## transform=mmult(translate(x,110), scale(1.2,1.2)), +## fill = color, +## ) +## ) +## +## +## return D +## +##def getDrawing4(): +## """This tests that attributes are 'unset' correctly when +## one steps back out of a drawing node. All the circles are part of a +## group setting the line color to blue; the second circle explicitly +## sets it to red. Ideally, the third circle should go back to blue.""" +## D = Drawing(400, 200) +## +## +## G = Group( +## Circle(100,100,20), +## Circle(200,100,20, stroke=colors.blue), +## Circle(300,100,20), +## stroke=colors.red, +## stroke_width=3, +## fill=colors.aqua +## ) +## D.add(G) +## +## +## D.add(String(10,50, 'Stack Unwinding - should be red, blue, red')) +## +## return D +## +## +##def getDrawing5(): +## """This Rotates Coordinate Axes""" +## D = Drawing(400, 200) +## +## +## +## Axis = Group( +## Line(0,0,100,0), #x axis +## Line(0,0,0,50), # y axis +## Line(0,10,10,10), #ticks on y axis +## Line(0,20,10,20), +## Line(0,30,10,30), +## Line(0,40,10,40), +## Line(10,0,10,10), #ticks on x axis +## Line(20,0,20,10), +## Line(30,0,30,10), +## Line(40,0,40,10), +## Line(50,0,50,10), +## Line(60,0,60,10), +## Line(70,0,70,10), +## Line(80,0,80,10), +## Line(90,0,90,10), +## String(20, 35, 'Axes', fill=colors.black) +## ) +## +## D.addDef('Axes', Axis) +## +## D.add(NamedReference('Axis', Axis, +## transform=translate(10,10))) +## D.add(NamedReference('Axis', Axis, +## transform=mmult(translate(150,10),rotate(15))) +## ) +## return D +## +##def getDrawing6(): +## """This Rotates Text""" +## D = Drawing(400, 300, fill=colors.black) +## +## xform = translate(200,150) +## C = (colors.black,colors.red,colors.green,colors.blue,colors.brown,colors.gray, colors.pink, +## colors.lavender,colors.lime, colors.mediumblue, colors.magenta, colors.limegreen) +## +## for i in range(12): +## D.add(String(0, 0, ' - - Rotated Text', fill=C[i%len(C)], transform=mmult(xform, rotate(30*i)))) +## +## return D +## +##def getDrawing7(): +## """This defines and tests a simple UserNode0 (the trailing zero denotes +## an experimental method which is not part of the supported API yet). +## Each of the four charts is a subclass of UserNode which generates a random +## series when rendered.""" +## +## class MyUserNode(UserNode0): +## import whrandom, math +## +## +## def provideNode(self, sender): +## """draw a simple chart that changes everytime it's drawn""" +## # print "here's a random number %s" % self.whrandom.random() +## #print "MyUserNode.provideNode being called by %s" % sender +## g = Group() +## #g._state = self._state # this is naughty +## PingoNode.__init__(g, self._state) # is this less naughty ? +## w = 80.0 +## h = 50.0 +## g.add(Rect(0,0, w, h, stroke=colors.black)) +## N = 10.0 +## x,y = (0,h) +## dx = w/N +## for ii in range(N): +## dy = (h/N) * self.whrandom.random() +## g.add(Line(x,y,x+dx, y-dy)) +## x = x + dx +## y = y - dy +## return g +## +## D = Drawing(400,200, fill=colors.white) # AR - same size as others +## +## D.add(MyUserNode()) +## +## graphcolor= [colors.green, colors.red, colors.brown, colors.purple] +## for ii in range(4): +## D.add(Group( MyUserNode(stroke=graphcolor[ii], stroke_width=2), +## transform=translate(ii*90,0) )) +## +## #un = MyUserNode() +## #print un.provideNode() +## return D +## +##def getDrawing8(): +## """Test Path operations--lineto, curveTo, etc.""" +## D = Drawing(400, 200, fill=None, stroke=colors.purple, stroke_width=2) +## +## xform = translate(200,100) +## C = (colors.black,colors.red,colors.green,colors.blue,colors.brown,colors.gray, colors.pink, +## colors.lavender,colors.lime, colors.mediumblue, colors.magenta, colors.limegreen) +## p = Path(50,50) +## p.lineTo(100,100) +## p.moveBy(-25,25) +## p.curveTo(150,125, 125,125, 200,50) +## p.curveTo(175, 75, 175, 98, 62, 87) +## +## +## D.add(p) +## D.add(String(10,30, 'Tests of path elements-lines and bezier curves-and text formating')) +## D.add(Line(220,150, 220,200, stroke=colors.red)) +## D.add(String(220,180, "Text should be centered", text_anchor="middle") ) +## +## +## return D + + +if __name__=='__main__': + print __doc__
\ No newline at end of file diff --git a/bin/reportlab/graphics/testshapes.py b/bin/reportlab/graphics/testshapes.py new file mode 100755 index 00000000000..f73d15ba0a9 --- /dev/null +++ b/bin/reportlab/graphics/testshapes.py @@ -0,0 +1,547 @@ +#! /usr/bin/python2.3 +#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/testshapes.py + +# testshapes.py - draws shapes onto a PDF canvas. + +""" +Execute the script to see some test drawings. + +This contains a number of routines to generate test drawings +for reportlab/graphics. For now many of them are contrived, +but we will expand them to try and trip up any parser. +Feel free to add more. +""" + +__version__ = ''' $Id $ ''' + + +import os, sys + +from reportlab.lib import colors +from reportlab.lib.units import cm +from reportlab.pdfgen.canvas import Canvas +from reportlab.pdfbase.pdfmetrics import stringWidth +from reportlab.platypus import Flowable +from reportlab.graphics.shapes import * +from reportlab.graphics.renderPDF import _PDFRenderer +import unittest + +_FONTS = ['Times-Roman','Courier','Times-BoldItalic',] + +######################################################### +# +# Collections of shape drawings. +# +######################################################### + + +def getFailedDrawing(funcName): + """Generate a drawing in case something goes really wrong. + + This will create a drawing to be displayed whenever some + other drawing could not be executed, because the generating + function does something terribly wrong! The box contains + an attention triangle, plus some error message. + """ + + D = Drawing(400, 200) + + points = [200,170, 140,80, 260,80] + D.add(Polygon(points, + strokeWidth=0.5*cm, + strokeColor=colors.red, + fillColor=colors.yellow)) + + s = String(200, 40, + "Error in generating function '%s'!" % funcName, + textAnchor='middle') + D.add(s) + + return D + + +# These are the real drawings to be eye-balled. + +def getDrawing01(): + """Hello World, on a rectangular background. + + The rectangle's fillColor is yellow. + The string's fillColor is red. + """ + + D = Drawing(400, 200) + D.add(Rect(50, 50, 300, 100, fillColor=colors.yellow)) + D.add(String(180,100, 'Hello World', fillColor=colors.red)) + + return D + + +def getDrawing02(): + """Various Line shapes. + + The lines are blue and their strokeWidth is 5 mm. + One line has a strokeDashArray set to [5, 10, 15]. + """ + + D = Drawing(400, 200) + D.add(Line(50,50, 300,100, + strokeColor=colors.blue, + strokeWidth=0.5*cm, + )) + D.add(Line(50,100, 300,50, + strokeColor=colors.blue, + strokeWidth=0.5*cm, + strokeDashArray=[5, 10, 15], + )) + + #x = 1/0 # Comment this to see the actual drawing! + + return D + + +def getDrawing03(): + """Text strings in various sizes and different fonts. + + Font size increases from 12 to 36 and from bottom left + to upper right corner. The first ones should be in + Times-Roman. Finally, a solitary Courier string at + the top right corner. + """ + + D = Drawing(400, 200) + for size in range(12, 36, 4): + D.add(String(10+size*2, + 10+size*2, + 'Hello World', + fontName=_FONTS[0], + fontSize=size)) + + D.add(String(150, 150, + 'Hello World', + fontName=_FONTS[1], + fontSize=36)) + return D + + +def getDrawing04(): + """Text strings in various colours. + + Colours are blue, yellow and red from bottom left + to upper right. + """ + + D = Drawing(400, 200) + i = 0 + for color in (colors.blue, colors.yellow, colors.red): + D.add(String(50+i*30, 50+i*30, + 'Hello World', fillColor=color)) + i = i + 1 + + return D + + +def getDrawing05(): + """Text strings with various anchors (alignments). + + Text alignment conforms to the anchors in the left column. + """ + + D = Drawing(400, 200) + + lineX = 250 + D.add(Line(lineX,10, lineX,190, strokeColor=colors.gray)) + + y = 130 + for anchor in ('start', 'middle', 'end'): + D.add(String(lineX, y, 'Hello World', textAnchor=anchor)) + D.add(String(50, y, anchor + ':')) + y = y - 30 + + return D + + +def getDrawing06(): + """This demonstrates all the basic shapes at once. + + There are no groups or references. + Each solid shape should have a purple fill. + """ + + purple = colors.purple + purple = colors.green + + D = Drawing(400, 200) #, fillColor=purple) + + D.add(Line(10,10, 390,190)) + + D.add(Circle(100,100,20, fillColor=purple)) + D.add(Circle(200,100,40, fillColor=purple)) + D.add(Circle(300,100,30, fillColor=purple)) + + D.add(Wedge(330,100,40, -10,40, fillColor=purple)) + + D.add(PolyLine([120,10, 130,20, 140,10, 150,20, 160,10, + 170,20, 180,10, 190,20, 200,10], fillColor=purple)) + + D.add(Polygon([300,20, 350,20, 390,80, 300,75, 330,40], fillColor=purple)) + + D.add(Ellipse(50,150, 40, 20, fillColor=purple)) + + D.add(Rect(120,150, 60,30, + strokeWidth=10, + strokeColor=colors.yellow, + fillColor=purple)) #square corners + + D.add(Rect(220, 150, 60, 30, 10, 10, fillColor=purple)) #round corners + + D.add(String(10,50, 'Basic Shapes', fillColor=colors.black)) + + return D + +def getDrawing07(): + """This tests the ability to translate and rotate groups. The first set of axes should be + near the bottom left of the drawing. The second should be rotated counterclockwise + by 15 degrees. The third should be rotated by 30 degrees.""" + D = Drawing(400, 200) + + Axis = Group( + Line(0,0,100,0), #x axis + Line(0,0,0,50), # y axis + Line(0,10,10,10), #ticks on y axis + Line(0,20,10,20), + Line(0,30,10,30), + Line(0,40,10,40), + Line(10,0,10,10), #ticks on x axis + Line(20,0,20,10), + Line(30,0,30,10), + Line(40,0,40,10), + Line(50,0,50,10), + Line(60,0,60,10), + Line(70,0,70,10), + Line(80,0,80,10), + Line(90,0,90,10), + String(20, 35, 'Axes', fill=colors.black) + ) + + firstAxisGroup = Group(Axis) + firstAxisGroup.translate(10,10) + D.add(firstAxisGroup) + + secondAxisGroup = Group(Axis) + secondAxisGroup.translate(150,10) + secondAxisGroup.rotate(15) + + D.add(secondAxisGroup) + + + thirdAxisGroup = Group(Axis, transform=mmult(translate(300,10), rotate(30))) + D.add(thirdAxisGroup) + + return D + + +def getDrawing08(): + """This tests the ability to scale coordinates. The bottom left set of axes should be + near the bottom left of the drawing. The bottom right should be stretched vertically + by a factor of 2. The top left one should be stretched horizontally by a factor of 2. + The top right should have the vertical axiss leaning over to the right by 30 degrees.""" + D = Drawing(400, 200) + + Axis = Group( + Line(0,0,100,0), #x axis + Line(0,0,0,50), # y axis + Line(0,10,10,10), #ticks on y axis + Line(0,20,10,20), + Line(0,30,10,30), + Line(0,40,10,40), + Line(10,0,10,10), #ticks on x axis + Line(20,0,20,10), + Line(30,0,30,10), + Line(40,0,40,10), + Line(50,0,50,10), + Line(60,0,60,10), + Line(70,0,70,10), + Line(80,0,80,10), + Line(90,0,90,10), + String(20, 35, 'Axes', fill=colors.black) + ) + + firstAxisGroup = Group(Axis) + firstAxisGroup.translate(10,10) + D.add(firstAxisGroup) + + secondAxisGroup = Group(Axis) + secondAxisGroup.translate(150,10) + secondAxisGroup.scale(1,2) + D.add(secondAxisGroup) + + thirdAxisGroup = Group(Axis) + thirdAxisGroup.translate(10,125) + thirdAxisGroup.scale(2,1) + D.add(thirdAxisGroup) + + fourthAxisGroup = Group(Axis) + fourthAxisGroup.translate(250,125) + fourthAxisGroup.skew(30,0) + D.add(fourthAxisGroup) + + + return D + +def getDrawing09(): + """This tests rotated strings + + Some renderers will have a separate mechanism for font drawing. This test + just makes sure strings get transformed the same way as regular graphics.""" + D = Drawing(400, 200) + + fontName = _FONTS[0] + fontSize = 12 + text = "I should be totally horizontal and enclosed in a box" + textWidth = stringWidth(text, fontName, fontSize) + + + g1 = Group( + String(20, 20, text, fontName=fontName, fontSize = fontSize), + Rect(18, 18, textWidth + 4, fontSize + 4, fillColor=None) + ) + D.add(g1) + + text = "I should slope up by 15 degrees, so my right end is higher than my left" + textWidth = stringWidth(text, fontName, fontSize) + g2 = Group( + String(20, 20, text, fontName=fontName, fontSize = fontSize), + Rect(18, 18, textWidth + 4, fontSize + 4, fillColor=None) + ) + g2.translate(0, 50) + g2.rotate(15) + D.add(g2) + + return D + +def getDrawing10(): + """This tests nested groups with multiple levels of coordinate transformation. + Each box should be staggered up and to the right, moving by 25 points each time.""" + D = Drawing(400, 200) + + fontName = _FONTS[0] + fontSize = 12 + + g1 = Group( + Rect(0, 0, 100, 20, fillColor=colors.yellow), + String(5, 5, 'Text in the box', fontName=fontName, fontSize = fontSize) + ) + D.add(g1) + + g2 = Group(g1, transform = translate(25,25)) + D.add(g2) + + g3 = Group(g2, transform = translate(25,25)) + D.add(g3) + + g4 = Group(g3, transform = translate(25,25)) + D.add(g4) + + + return D + +from widgets.signsandsymbols import SmileyFace +def getDrawing11(): + '''test of anchoring''' + def makeSmiley(x, y, size, color): + "Make a smiley data item representation." + d = size + s = SmileyFace() + s.fillColor = color + s.x = x-d + s.y = y-d + s.size = d*2 + return s + + D = Drawing(400, 200) #, fillColor=colors.purple) + g = Group(transform=(1,0,0,1,0,0)) + g.add(makeSmiley(100,100,10,colors.red)) + g.add(Line(90,100,110,100,strokeColor=colors.green)) + g.add(Line(100,90,100,110,strokeColor=colors.green)) + D.add(g) + g = Group(transform=(2,0,0,2,100,-100)) + g.add(makeSmiley(100,100,10,colors.blue)) + g.add(Line(90,100,110,100,strokeColor=colors.green)) + g.add(Line(100,90,100,110,strokeColor=colors.green)) + D.add(g) + g = Group(transform=(2,0,0,2,0,0)) + return D + + +def getDrawing12(): + """Text strings in a non-standard font. + All that is required is to place the .afm and .pfb files + on the font patch given in rl_config.py, + for example in reportlab/lib/fonts/. + """ + faceName = "Wargames-Regular" + D = Drawing(400, 200) + for size in range(12, 36, 4): + D.add(String(10+size*2, + 10+size*2, + 'Hello World', + fontName=faceName, + fontSize=size)) + return D + +def getDrawing13(): + 'Test Various TTF Fonts' + from reportlab.pdfbase import pdfmetrics, ttfonts + pdfmetrics.registerFont(ttfonts.TTFont("LuxiSerif", "luxiserif.ttf")) + pdfmetrics.registerFont(ttfonts.TTFont("Rina", "rina.ttf")) + _FONTS[1] = 'LuxiSerif' + _FONTS[2] = 'Rina' + F = ['Times-Roman','LuxiSerif', 'Rina'] + if sys.platform=='win32': + for name, ttf in [('Adventurer Light SF','Advlit.ttf'),('ArialMS','ARIAL.TTF'), + ('Book Antiqua','BKANT.TTF'), + ('Century Gothic','GOTHIC.TTF'), + ('Comic Sans MS', 'COMIC.TTF'), + ('Elementary Heavy SF Bold','Vwagh.ttf'), + ('Firenze SF','flot.ttf'), + ('Garamond','GARA.TTF'), + ('Jagger','Rols.ttf'), + ('Monotype Corsiva','MTCORSVA.TTF'), + ('Seabird SF','seag.ttf'), + ('Tahoma','TAHOMA.TTF'), + ('VerdanaMS','VERDANA.TTF'), + ]: + for D in ('c:\WINNT','c:\Windows'): + fn = os.path.join(D,'Fonts',ttf) + if os.path.isfile(fn): + try: + f = ttfonts.TTFont(name, fn) + pdfmetrics.registerFont(f) + F.append(name) + except: + pass + + def drawit(F,w=400,h=200,fontSize=12,slack=2,gap=5): + D = Drawing(w,h) + th = 2*gap + fontSize*1.2 + gh = gap + .2*fontSize + y = h + maxx = 0 + for fontName in F: + y -= th + text = fontName+": I should be totally horizontal and enclosed in a box" + textWidth = stringWidth(text, fontName, fontSize) + maxx = max(maxx,textWidth+20) + D.add( + Group(Rect(8, y-gh, textWidth + 4, th, strokeColor=colors.red, strokeWidth=.5, fillColor=colors.lightgrey), + String(10, y, text, fontName=fontName, fontSize = fontSize))) + y -= 5 + return maxx, h-y+gap, D + maxx, maxy, D = drawit(F) + if maxx>400 or maxy>200: _,_,D = drawit(F,maxx,maxy) + return D + +def getAllFunctionDrawingNames(doTTF=1): + "Get a list of drawing function names from somewhere." + + funcNames = [] + + # Here we get the names from the global name space. + symbols = globals().keys() + symbols.sort() + for funcName in symbols: + if funcName[0:10] == 'getDrawing': + if doTTF or funcName!='getDrawing13': + funcNames.append(funcName) + + return funcNames + +def _evalFuncDrawing(name, D, l=None, g=None): + try: + d = eval(name + '()', g or globals(), l or locals()) + except: + d = getFailedDrawing(name) + D.append((d, eval(name + '.__doc__'), name[3:])) + +def getAllTestDrawings(doTTF=1): + D = [] + for f in getAllFunctionDrawingNames(doTTF=doTTF): + _evalFuncDrawing(f,D) + return D + +def writePDF(drawings): + "Create and save a PDF file containing some drawings." + + pdfPath = os.path.splitext(sys.argv[0])[0] + '.pdf' + c = Canvas(pdfPath) + c.setFont(_FONTS[0], 32) + c.drawString(80, 750, 'ReportLab Graphics-Shapes Test') + + # Print drawings in a loop, with their doc strings. + c.setFont(_FONTS[0], 12) + y = 740 + i = 1 + for (drawing, docstring, funcname) in drawings: + if y < 300: # Allows 5-6 lines of text. + c.showPage() + y = 740 + # Draw a title. + y = y - 30 + c.setFont(_FONTS[2],12) + c.drawString(80, y, '%s (#%d)' % (funcname, i)) + c.setFont(_FONTS[0],12) + y = y - 14 + textObj = c.beginText(80, y) + textObj.textLines(docstring) + c.drawText(textObj) + y = textObj.getY() + y = y - drawing.height + drawing.drawOn(c, 80, y) + i = i + 1 + + c.save() + print 'wrote %s ' % pdfPath + + +class ShapesTestCase(unittest.TestCase): + "Test generating all kinds of shapes." + + def setUp(self): + "Prepare some things before the tests start." + + self.funcNames = getAllFunctionDrawingNames() + self.drawings = [] + + + def tearDown(self): + "Do what has to be done after the tests are over." + + writePDF(self.drawings) + + + # This should always succeed. If each drawing would be + # wrapped in a dedicated test method like this one, it + # would be possible to have a count for wrong tests + # as well... Something like this is left for later... + def testAllDrawings(self): + "Make a list of drawings." + + for f in self.funcNames: + if f[0:10] == 'getDrawing': + # Make an instance and get its doc string. + # If that fails, use a default error drawing. + _evalFuncDrawing(f,self.drawings) + + +def makeSuite(): + "Make a test suite for unit testing." + + suite = unittest.TestSuite() + suite.addTest(ShapesTestCase('testAllDrawings')) + return suite + + +if __name__ == "__main__": + unittest.TextTestRunner().run(makeSuite()) diff --git a/bin/reportlab/graphics/widgetbase.py b/bin/reportlab/graphics/widgetbase.py new file mode 100644 index 00000000000..db40ee219cb --- /dev/null +++ b/bin/reportlab/graphics/widgetbase.py @@ -0,0 +1,490 @@ +#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/widgetbase.py +__version__=''' $Id$ ''' +import string + +from reportlab.graphics import shapes +from reportlab import rl_config +from reportlab.lib import colors +from reportlab.lib.validators import * +from reportlab.lib.attrmap import * + + +class PropHolder: + '''Base for property holders''' + + _attrMap = None + + def verify(self): + """If the _attrMap attribute is not None, this + checks all expected attributes are present; no + unwanted attributes are present; and (if a + checking function is found) checks each + attribute has a valid value. Either succeeds + or raises an informative exception. + """ + + if self._attrMap is not None: + for key in self.__dict__.keys(): + if key[0] <> '_': + msg = "Unexpected attribute %s found in %s" % (key, self) + assert self._attrMap.has_key(key), msg + for (attr, metavalue) in self._attrMap.items(): + msg = "Missing attribute %s from %s" % (attr, self) + assert hasattr(self, attr), msg + value = getattr(self, attr) + args = (value, attr, self.__class__.__name__) + assert metavalue.validate(value), "Invalid value %s for attribute %s in class %s" % args + + if rl_config.shapeChecking: + """This adds the ability to check every attribute assignment + as it is made. It slows down shapes but is a big help when + developing. It does not get defined if rl_config.shapeChecking = 0. + """ + + def __setattr__(self, name, value): + """By default we verify. This could be off + in some parallel base classes.""" + validateSetattr(self,name,value) + + + def getProperties(self,recur=1): + """Returns a list of all properties which can be edited and + which are not marked as private. This may include 'child + widgets' or 'primitive shapes'. You are free to override + this and provide alternative implementations; the default + one simply returns everything without a leading underscore. + """ + + from reportlab.lib.validators import isValidChild + + # TODO when we need it, but not before - + # expose sequence contents? + + props = {} + for name in self.__dict__.keys(): + if name[0:1] <> '_': + component = getattr(self, name) + + if recur and isValidChild(component): + # child object, get its properties too + childProps = component.getProperties(recur=recur) + for (childKey, childValue) in childProps.items(): + #key might be something indexed like '[2].fillColor' + #or simple like 'fillColor'; in the former case we + #don't need a '.' between me and my child. + if childKey[0] == '[': + props['%s%s' % (name, childKey)] = childValue + else: + props['%s.%s' % (name, childKey)] = childValue + else: + props[name] = component + + return props + + + def setProperties(self, propDict): + """Permits bulk setting of properties. These may include + child objects e.g. "chart.legend.width = 200". + + All assignments will be validated by the object as if they + were set individually in python code. + + All properties of a top-level object are guaranteed to be + set before any of the children, which may be helpful to + widget designers. + """ + + childPropDicts = {} + for (name, value) in propDict.items(): + parts = string.split(name, '.', 1) + if len(parts) == 1: + #simple attribute, set it now + setattr(self, name, value) + else: + (childName, remains) = parts + try: + childPropDicts[childName][remains] = value + except KeyError: + childPropDicts[childName] = {remains: value} + + # now assign to children + for (childName, childPropDict) in childPropDicts.items(): + child = getattr(self, childName) + child.setProperties(childPropDict) + + + def dumpProperties(self, prefix=""): + """Convenience. Lists them on standard output. You + may provide a prefix - mostly helps to generate code + samples for documentation. + """ + + propList = self.getProperties().items() + propList.sort() + if prefix: + prefix = prefix + '.' + for (name, value) in propList: + print '%s%s = %s' % (prefix, name, value) + + +class Widget(PropHolder, shapes.UserNode): + """Base for all user-defined widgets. Keep as simple as possible. Does + not inherit from Shape so that we can rewrite shapes without breaking + widgets and vice versa.""" + + def _setKeywords(self,**kw): + for k,v in kw.items(): + if not self.__dict__.has_key(k): + setattr(self,k,v) + + def draw(self): + msg = "draw() must be implemented for each Widget!" + raise shapes.NotImplementedError, msg + + def demo(self): + msg = "demo() must be implemented for each Widget!" + raise shapes.NotImplementedError, msg + + def provideNode(self): + return self.draw() + + def getBounds(self): + "Return outer boundary as x1,y1,x2,y2. Can be overridden for efficiency" + return self.draw().getBounds() + +_ItemWrapper={} + +class TypedPropertyCollection(PropHolder): + """A container with properties for objects of the same kind. + + This makes it easy to create lists of objects. You initialize + it with a class of what it is to contain, and that is all you + can add to it. You can assign properties to the collection + as a whole, or to a numeric index within it; if so it creates + a new child object to hold that data. + + So: + wedges = TypedPropertyCollection(WedgeProperties) + wedges.strokeWidth = 2 # applies to all + wedges.strokeColor = colors.red # applies to all + wedges[3].strokeColor = colors.blue # only to one + + The last line should be taken as a prescription of how to + create wedge no. 3 if one is needed; no error is raised if + there are only two data points. + """ + + def __init__(self, exampleClass): + #give it same validation rules as what it holds + self.__dict__['_value'] = exampleClass() + self.__dict__['_children'] = {} + + def __getitem__(self, index): + try: + return self._children[index] + except KeyError: + Klass = self._value.__class__ + if _ItemWrapper.has_key(Klass): + WKlass = _ItemWrapper[Klass] + else: + class WKlass(Klass): + def __getattr__(self,name): + try: + return self.__class__.__bases__[0].__getattr__(self,name) + except: + if self._index and self._parent._children.has_key(self._index): + if self._parent._children[self._index].__dict__.has_key(name): + return getattr(self._parent._children[self._index],name) + return getattr(self._parent,name) + _ItemWrapper[Klass] = WKlass + + child = WKlass() + child._parent = self + if type(index) in (type(()),type([])): + index = tuple(index) + if len(index)>1: + child._index = tuple(index[:-1]) + else: + child._index = None + else: + child._index = None + for i in filter(lambda x,K=child.__dict__.keys(): x in K,child._attrMap.keys()): + del child.__dict__[i] + + self._children[index] = child + return child + + def has_key(self,key): + if type(key) in (type(()),type([])): key = tuple(key) + return self._children.has_key(key) + + def __setitem__(self, key, value): + msg = "This collection can only hold objects of type %s" % self._value.__class__.__name__ + assert isinstance(value, self._value.__class__), msg + + def __len__(self): + return len(self._children.keys()) + + def getProperties(self,recur=1): + # return any children which are defined and whatever + # differs from the parent + props = {} + + for (key, value) in self._value.getProperties(recur=recur).items(): + props['%s' % key] = value + + for idx in self._children.keys(): + childProps = self._children[idx].getProperties(recur=recur) + for (key, value) in childProps.items(): + if not hasattr(self,key) or getattr(self, key)<>value: + newKey = '[%s].%s' % (idx, key) + props[newKey] = value + return props + + def setVector(self,**kw): + for name, value in kw.items(): + for i in xrange(len(value)): + setattr(self[i],name,value[i]) + + def __getattr__(self,name): + return getattr(self._value,name) + + def __setattr__(self,name,value): + return setattr(self._value,name,value) + +## No longer needed! +class StyleProperties(PropHolder): + """A container class for attributes used in charts and legends. + + Attributes contained can be those for any graphical element + (shape?) in the ReportLab graphics package. The idea for this + container class is to be useful in combination with legends + and/or the individual appearance of data series in charts. + + A legend could be as simple as a wrapper around a list of style + properties, where the 'desc' attribute contains a descriptive + string and the rest could be used by the legend e.g. to draw + something like a color swatch. The graphical presentation of + the legend would be its own business, though. + + A chart could be inspecting a legend or, more directly, a list + of style properties to pick individual attributes that it knows + about in order to render a particular row of the data. A bar + chart e.g. could simply use 'strokeColor' and 'fillColor' for + drawing the bars while a line chart could also use additional + ones like strokeWidth. + """ + + _attrMap = AttrMap( + strokeWidth = AttrMapValue(isNumber), + strokeLineCap = AttrMapValue(isNumber), + strokeLineJoin = AttrMapValue(isNumber), + strokeMiterLimit = AttrMapValue(None), + strokeDashArray = AttrMapValue(isListOfNumbersOrNone), + strokeOpacity = AttrMapValue(isNumber), + strokeColor = AttrMapValue(isColorOrNone), + fillColor = AttrMapValue(isColorOrNone), + desc = AttrMapValue(isString), + ) + + def __init__(self, **kwargs): + "Initialize with attributes if any." + + for k, v in kwargs.items(): + setattr(self, k, v) + + + def __setattr__(self, name, value): + "Verify attribute name and value, before setting it." + validateSetattr(self,name,value) + + +class TwoCircles(Widget): + def __init__(self): + self.leftCircle = shapes.Circle(100,100,20, fillColor=colors.red) + self.rightCircle = shapes.Circle(300,100,20, fillColor=colors.red) + + def draw(self): + return shapes.Group(self.leftCircle, self.rightCircle) + + +class Face(Widget): + """This draws a face with two eyes. + + It exposes a couple of properties + to configure itself and hides all other details. + """ + + _attrMap = AttrMap( + x = AttrMapValue(isNumber), + y = AttrMapValue(isNumber), + size = AttrMapValue(isNumber), + skinColor = AttrMapValue(isColorOrNone), + eyeColor = AttrMapValue(isColorOrNone), + mood = AttrMapValue(OneOf('happy','sad','ok')), + ) + + def __init__(self): + self.x = 10 + self.y = 10 + self.size = 80 + self.skinColor = None + self.eyeColor = colors.blue + self.mood = 'happy' + + def demo(self): + pass + + def draw(self): + s = self.size # abbreviate as we will use this a lot + g = shapes.Group() + g.transform = [1,0,0,1,self.x, self.y] + + # background + g.add(shapes.Circle(s * 0.5, s * 0.5, s * 0.5, fillColor=self.skinColor)) + + # left eye + g.add(shapes.Circle(s * 0.35, s * 0.65, s * 0.1, fillColor=colors.white)) + g.add(shapes.Circle(s * 0.35, s * 0.65, s * 0.05, fillColor=self.eyeColor)) + + # right eye + g.add(shapes.Circle(s * 0.65, s * 0.65, s * 0.1, fillColor=colors.white)) + g.add(shapes.Circle(s * 0.65, s * 0.65, s * 0.05, fillColor=self.eyeColor)) + + # nose + g.add(shapes.Polygon( + points=[s * 0.5, s * 0.6, s * 0.4, s * 0.3, s * 0.6, s * 0.3], + fillColor=None)) + + # mouth + if self.mood == 'happy': + offset = -0.05 + elif self.mood == 'sad': + offset = +0.05 + else: + offset = 0 + + g.add(shapes.Polygon( + points = [ + s * 0.3, s * 0.2, #left of mouth + s * 0.7, s * 0.2, #right of mouth + s * 0.6, s * (0.2 + offset), # the bit going up or down + s * 0.4, s * (0.2 + offset) # the bit going up or down + ], + fillColor = colors.pink, + strokeColor = colors.red, + strokeWidth = s * 0.03 + )) + + return g + + +class TwoFaces(Widget): + def __init__(self): + self.faceOne = Face() + self.faceOne.mood = "happy" + self.faceTwo = Face() + self.faceTwo.x = 100 + self.faceTwo.mood = "sad" + + def draw(self): + """Just return a group""" + return shapes.Group(self.faceOne, self.faceTwo) + + def demo(self): + """The default case already looks good enough, + no implementation needed here""" + pass + +class Sizer(Widget): + "Container to show size of all enclosed objects" + + _attrMap = AttrMap(BASE=shapes.SolidShape, + contents = AttrMapValue(isListOfShapes,desc="Contained drawable elements"), + ) + def __init__(self, *elements): + self.contents = [] + self.fillColor = colors.cyan + self.strokeColor = colors.magenta + + for elem in elements: + self.add(elem) + + def _addNamedNode(self,name,node): + 'if name is not None add an attribute pointing to node and add to the attrMap' + if name: + if name not in self._attrMap.keys(): + self._attrMap[name] = AttrMapValue(isValidChild) + setattr(self, name, node) + + def add(self, node, name=None): + """Appends non-None child node to the 'contents' attribute. In addition, + if a name is provided, it is subsequently accessible by name + """ + # propagates properties down + if node is not None: + assert isValidChild(node), "Can only add Shape or UserNode objects to a Group" + self.contents.append(node) + self._addNamedNode(name,node) + + def getBounds(self): + # get bounds of each object + if self.contents: + b = [] + for elem in self.contents: + b.append(elem.getBounds()) + return shapes.getRectsBounds(b) + else: + return (0,0,0,0) + + def draw(self): + g = shapes.Group() + (x1, y1, x2, y2) = self.getBounds() + r = shapes.Rect( + x = x1, + y = y1, + width = x2-x1, + height = y2-y1, + fillColor = self.fillColor, + strokeColor = self.strokeColor + ) + g.add(r) + for elem in self.contents: + g.add(elem) + return g + +def test(): + from reportlab.graphics.charts.piecharts import WedgeProperties + wedges = TypedPropertyCollection(WedgeProperties) + wedges.fillColor = colors.red + wedges.setVector(fillColor=(colors.blue,colors.green,colors.white)) + print len(_ItemWrapper) + + d = shapes.Drawing(400, 200) + tc = TwoCircles() + d.add(tc) + import renderPDF + renderPDF.drawToFile(d, 'sample_widget.pdf', 'A Sample Widget') + print 'saved sample_widget.pdf' + + d = shapes.Drawing(400, 200) + f = Face() + f.skinColor = colors.yellow + f.mood = "sad" + d.add(f, name='theFace') + print 'drawing 1 properties:' + d.dumpProperties() + renderPDF.drawToFile(d, 'face.pdf', 'A Sample Widget') + print 'saved face.pdf' + + d2 = d.expandUserNodes() + renderPDF.drawToFile(d2, 'face_copy.pdf', 'An expanded drawing') + print 'saved face_copy.pdf' + print 'drawing 2 properties:' + d2.dumpProperties() + + +if __name__=='__main__': + test() diff --git a/bin/reportlab/graphics/widgets/__init__.py b/bin/reportlab/graphics/widgets/__init__.py new file mode 100644 index 00000000000..ca91d242511 --- /dev/null +++ b/bin/reportlab/graphics/widgets/__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/widgets/__init__.py +__version__=''' $Id$ '''
\ No newline at end of file diff --git a/bin/reportlab/graphics/widgets/eventcal.py b/bin/reportlab/graphics/widgets/eventcal.py new file mode 100644 index 00000000000..677081b2d8a --- /dev/null +++ b/bin/reportlab/graphics/widgets/eventcal.py @@ -0,0 +1,303 @@ +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/widgets/eventcal.py +# Event Calendar widget +# author: Andy Robinson +"""This file is a +""" +__version__=''' $Id$ ''' + +from reportlab.lib import colors +from reportlab.lib.validators import * +from reportlab.lib.attrmap import * +from reportlab.graphics.shapes import Line, Rect, Polygon, Drawing, Group, String, Circle, Wedge +from reportlab.graphics.charts.textlabels import Label +from reportlab.graphics.widgetbase import Widget +from reportlab.graphics import renderPDF + + + + +class EventCalendar(Widget): + def __init__(self): + self.x = 0 + self.y = 0 + self.width = 300 + self.height = 150 + self.timeColWidth = None # if declared, use it; otherwise auto-size. + self.trackRowHeight = 20 + self.data = [] # list of Event objects + self.trackNames = None + + self.startTime = None #displays ALL data on day if not set + self.endTime = None # displays ALL data on day if not set + self.day = 0 + + + # we will keep any internal geometry variables + # here. These are computed by computeSize(), + # which is the first thing done when drawing. + self._talksVisible = [] # subset of data which will get plotted, cache + self._startTime = None + self._endTime = None + self._trackCount = 0 + self._colWidths = [] + self._colLeftEdges = [] # left edge of each column + + def computeSize(self): + "Called at start of draw. Sets various column widths" + self._talksVisible = self.getRelevantTalks(self.data) + self._trackCount = len(self.getAllTracks()) + self.computeStartAndEndTimes() + self._colLeftEdges = [self.x] + if self.timeColWidth is None: + w = self.width / (1 + self._trackCount) + self._colWidths = [w] * (1+ self._trackCount) + for i in range(self._trackCount): + self._colLeftEdges.append(self._colLeftEdges[-1] + w) + else: + self._colWidths = [self.timeColWidth] + w = (self.width - self.timeColWidth) / self._trackCount + for i in range(self._trackCount): + self._colWidths.append(w) + self._colLeftEdges.append(self._colLeftEdges[-1] + w) + + + + def computeStartAndEndTimes(self): + "Work out first and last times to display" + if self.startTime: + self._startTime = self.startTime + else: + for (title, speaker, trackId, day, start, duration) in self._talksVisible: + + if self._startTime is None: #first one + self._startTime = start + else: + if start < self._startTime: + self._startTime = start + + if self.endTime: + self._endTime = self.endTime + else: + for (title, speaker, trackId, day, start, duration) in self._talksVisible: + if self._endTime is None: #first one + self._endTime = start + duration + else: + if start + duration > self._endTime: + self._endTime = start + duration + + + + + def getAllTracks(self): + tracks = [] + for (title, speaker, trackId, day, hours, duration) in self.data: + if trackId is not None: + if trackId not in tracks: + tracks.append(trackId) + tracks.sort() + return tracks + + def getRelevantTalks(self, talkList): + "Scans for tracks actually used" + used = [] + for talk in talkList: + (title, speaker, trackId, day, hours, duration) = talk + assert trackId <> 0, "trackId must be None or 1,2,3... zero not allowed!" + if day == self.day: + if (((self.startTime is None) or ((hours + duration) >= self.startTime)) + and ((self.endTime is None) or (hours <= self.endTime))): + used.append(talk) + return used + + def scaleTime(self, theTime): + "Return y-value corresponding to times given" + axisHeight = self.height - self.trackRowHeight + # compute fraction between 0 and 1, 0 is at start of period + proportionUp = ((theTime - self._startTime) / (self._endTime - self._startTime)) + y = self.y + axisHeight - (axisHeight * proportionUp) + return y + + + def getTalkRect(self, startTime, duration, trackId, text): + "Return shapes for a specific talk" + g = Group() + y_bottom = self.scaleTime(startTime + duration) + y_top = self.scaleTime(startTime) + y_height = y_top - y_bottom + + if trackId is None: + #spans all columns + x = self._colLeftEdges[1] + width = self.width - self._colWidths[0] + else: + #trackId is 1-based and these arrays have the margin info in column + #zero, so no need to add 1 + x = self._colLeftEdges[trackId] + width = self._colWidths[trackId] + + lab = Label() + lab.setText(text) + lab.setOrigin(x + 0.5*width, y_bottom+0.5*y_height) + lab.boxAnchor = 'c' + lab.width = width + lab.height = y_height + lab.fontSize = 6 + + r = Rect(x, y_bottom, width, y_height, fillColor=colors.cyan) + g.add(r) + g.add(lab) + + #now for a label + # would expect to color-code and add text + return g + + def draw(self): + self.computeSize() + g = Group() + + # time column + g.add(Rect(self.x, self.y, self._colWidths[0], self.height - self.trackRowHeight, fillColor=colors.cornsilk)) + + # track headers + x = self.x + self._colWidths[0] + y = self.y + self.height - self.trackRowHeight + for trk in range(self._trackCount): + wid = self._colWidths[trk+1] + r = Rect(x, y, wid, self.trackRowHeight, fillColor=colors.yellow) + s = String(x + 0.5*wid, y, 'Track %d' % trk, align='middle') + g.add(r) + g.add(s) + x = x + wid + + for talk in self._talksVisible: + (title, speaker, trackId, day, start, duration) = talk + r = self.getTalkRect(start, duration, trackId, title + '\n' + speaker) + g.add(r) + + + return g + + + + +def test(): + "Make a conference event for day 1 of UP Python 2003" + + + d = Drawing(400,200) + + cal = EventCalendar() + cal.x = 50 + cal.y = 25 + cal.data = [ + # these might be better as objects instead of tuples, since I + # predict a large number of "optionsl" variables to affect + # formatting in future. + + #title, speaker, track id, day, start time (hrs), duration (hrs) + # track ID is 1-based not zero-based! + ('Keynote: Why design another programming language?', 'Guido van Rossum', None, 1, 9.0, 1.0), + + ('Siena Web Service Architecture', 'Marc-Andre Lemburg', 1, 1, 10.5, 1.5), + ('Extreme Programming in Python', 'Chris Withers', 2, 1, 10.5, 1.5), + ('Pattern Experiences in C++', 'Mark Radford', 3, 1, 10.5, 1.5), + ('What is the Type of std::toupper()', 'Gabriel Dos Reis', 4, 1, 10.5, 1.5), + ('Linguistic Variables: Clear Thinking with Fuzzy Logic ', 'Walter Banks', 5, 1, 10.5, 1.5), + + ('lunch, short presentations, vendor presentations', '', None, 1, 12.0, 2.0), + + ("CORBA? Isn't that obsolete", 'Duncan Grisby', 1, 1, 14.0, 1.5), + ("Python Design Patterns", 'Duncan Booth', 2, 1, 14.0, 1.5), + ("Inside Security Checks and Safe Exceptions", 'Brandon Bray', 3, 1, 14.0, 1.5), + ("Studying at a Distance", 'Panel Discussion, Panel to include Alan Lenton & Francis Glassborow', 4, 1, 14.0, 1.5), + ("Coding Standards - Given the ANSI C Standard why do I still need a coding Standard", 'Randy Marques', 5, 1, 14.0, 1.5), + + ("RESTful Python", 'Hamish Lawson', 1, 1, 16.0, 1.5), + ("Parsing made easier - a radical old idea", 'Andrew Koenig', 2, 1, 16.0, 1.5), + ("C++ & Multimethods", 'Julian Smith', 3, 1, 16.0, 1.5), + ("C++ Threading", 'Kevlin Henney', 4, 1, 16.0, 1.5), + ("The Organisation Strikes Back", 'Alan Griffiths & Sarah Lees', 5, 1, 16.0, 1.5), + + ('Birds of a Feather meeting', '', None, 1, 17.5, 2.0), + + ('Keynote: In the Spirit of C', 'Greg Colvin', None, 2, 9.0, 1.0), + + ('The Infinite Filing Cabinet - object storage in Python', 'Jacob Hallen', 1, 2, 10.5, 1.5), + ('Introduction to Python and Jython for C++ and Java Programmers', 'Alex Martelli', 2, 2, 10.5, 1.5), + ('Template metaprogramming in Haskell', 'Simon Peyton Jones', 3, 2, 10.5, 1.5), + ('Plenty People Programming: C++ Programming in a Group, Workshop with a difference', 'Nico Josuttis', 4, 2, 10.5, 1.5), + ('Design and Implementation of the Boost Graph Library', 'Jeremy Siek', 5, 2, 10.5, 1.5), + + ('lunch, short presentations, vendor presentations', '', None, 2, 12.0, 2.0), + + ("Building GUI Applications with PythonCard and PyCrust", 'Andy Todd', 1, 2, 14.0, 1.5), + ("Integrating Python, C and C++", 'Duncan Booth', 2, 2, 14.0, 1.5), + ("Secrets and Pitfalls of Templates", 'Nicolai Josuttis & David Vandevoorde', 3, 2, 14.0, 1.5), + ("Being a Mentor", 'Panel Discussion, Panel to include Alan Lenton & Francis Glassborow', 4, 2, 14.0, 1.5), + ("The Embedded C Extensions to C", 'Willem Wakker', 5, 2, 14.0, 1.5), + + ("Lightning Talks", 'Paul Brian', 1, 2, 16.0, 1.5), + ("Scripting Java Applications with Jython", 'Anthony Eden', 2, 2, 16.0, 1.5), + ("Metaprogramming and the Boost Metaprogramming Library", 'David Abrahams', 3, 2, 16.0, 1.5), + ("A Common Vendor ABI for C++ -- GCC's why, what and not", 'Nathan Sidwell & Gabriel Dos Reis', 4, 2, 16.0, 1.5), + ("The Timing and Cost of Choices", 'Hubert Matthews', 5, 2, 16.0, 1.5), + + ('Birds of a Feather meeting', '', None, 2, 17.5, 2.0), + + ('Keynote: The Cost of C & C++ Compatibility', 'Andy Koenig', None, 3, 9.0, 1.0), + + ('Prying Eyes: Generic Observer Implementations in C++', 'Andrei Alexandrescu', 1, 2, 10.5, 1.5), + ('The Roadmap to Generative Programming With C++', 'Ulrich Eisenecker', 2, 2, 10.5, 1.5), + ('Design Patterns in C++ and C# for the Common Language Runtime', 'Brandon Bray', 3, 2, 10.5, 1.5), + ('Extreme Hour (XH): (workshop) - Jutta Eckstein and Nico Josuttis', 'Jutta Ecstein', 4, 2, 10.5, 1.5), + ('The Lambda Library : Unnamed Functions for C++', 'Jaako Jarvi', 5, 2, 10.5, 1.5), + + ('lunch, short presentations, vendor presentations', '', None, 3, 12.0, 2.0), + + ('Reflective Metaprogramming', 'Daveed Vandevoorde', 1, 3, 14.0, 1.5), + ('Advanced Template Issues and Solutions (double session)', 'Herb Sutter',2, 3, 14.0, 3), + ('Concurrent Programming in Java (double session)', 'Angelika Langer', 3, 3, 14.0, 3), + ('What can MISRA-C (2nd Edition) do for us?', 'Chris Hills', 4, 3, 14.0, 1.5), + ('C++ Metaprogramming Concepts and Results', 'Walter E Brown', 5, 3, 14.0, 1.5), + + ('Binding C++ to Python with the Boost Python Library', 'David Abrahams', 1, 3, 16.0, 1.5), + ('Using Aspect Oriented Programming for Enterprise Application Integration', 'Arno Schmidmeier', 4, 3, 16.0, 1.5), + ('Defective C++', 'Marc Paterno', 5, 3, 16.0, 1.5), + + ("Speakers' Banquet & Birds of a Feather meeting", '', None, 3, 17.5, 2.0), + + ('Keynote: The Internet, Software and Computers - A Report Card', 'Alan Lenton', None, 4, 9.0, 1.0), + + ('Multi-Platform Software Development; Lessons from the Boost libraries', 'Beman Dawes', 1, 5, 10.5, 1.5), + ('The Stability of the C++ ABI', 'Steve Clamage', 2, 5, 10.5, 1.5), + ('Generic Build Support - A Pragmatic Approach to the Software Build Process', 'Randy Marques', 3, 5, 10.5, 1.5), + ('How to Handle Project Managers: a survival guide', 'Barb Byro', 4, 5, 10.5, 1.5), + + ('lunch, ACCU AGM', '', None, 5, 12.0, 2.0), + + ('Sauce: An OO recursive descent parser; its design and implementation.', 'Jon Jagger', 1, 5, 14.0, 1.5), + ('GNIRTS ESAC REWOL - Bringing the UNIX filters to the C++ iostream library.', 'JC van Winkel', 2, 5, 14.0, 1.5), + ('Pattern Writing: Live and Direct', 'Frank Buschmann & Kevlin Henney', 3, 5, 14.0, 3.0), + ('The Future of Programming Languages - A Goldfish Bowl', 'Francis Glassborow and friends', 3, 5, 14.0, 1.5), + + ('Honey, I Shrunk the Threads: Compile-time checked multithreaded transactions in C++', 'Andrei Alexandrescu', 1, 5, 16.0, 1.5), + ('Fun and Functionality with Functors', 'Lois Goldthwaite', 2, 5, 16.0, 1.5), + ('Agile Enough?', 'Alan Griffiths', 4, 5, 16.0, 1.5), + ("Conference Closure: A brief plenary session", '', None, 5, 17.5, 0.5), + + ] + + #return cal + cal.day = 1 + + d.add(cal) + + + for format in ['pdf']:#,'gif','png']: + out = d.asString(format) + open('eventcal.%s' % format, 'wb').write(out) + print 'saved eventcal.%s' % format + +if __name__=='__main__': + test() diff --git a/bin/reportlab/graphics/widgets/flags.py b/bin/reportlab/graphics/widgets/flags.py new file mode 100644 index 00000000000..b8857066da8 --- /dev/null +++ b/bin/reportlab/graphics/widgets/flags.py @@ -0,0 +1,879 @@ +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/widgets/flags.py +# Flag Widgets - a collection of flags as widgets +# author: John Precedo (johnp@reportlab.com) +"""This file is a collection of flag graphics as widgets. + +All flags are represented at the ratio of 1:2, even where the official ratio for the flag is something else +(such as 3:5 for the German national flag). The only exceptions are for where this would look _very_ wrong, +such as the Danish flag whose (ratio is 28:37), or the Swiss flag (which is square). + +Unless otherwise stated, these flags are all the 'national flags' of the countries, rather than their +state flags, naval flags, ensigns or any other variants. (National flags are the flag flown by civilians +of a country and the ones usually used to represent a country abroad. State flags are the variants used by +the government and by diplomatic missions overseas). + +To check on how close these are to the 'official' representations of flags, check the World Flag Database at +http://www.flags.ndirect.co.uk/ + +The flags this file contains are: + +EU Members: +United Kingdom, Austria, Belgium, Denmark, Finland, France, Germany, Greece, Ireland, Italy, Luxembourg, +Holland (The Netherlands), Spain, Sweden + +Others: +USA, Czech Republic, European Union, Switzerland, Turkey, Brazil + +(Brazilian flag contributed by Publio da Costa Melo [publio@planetarium.com.br]). +""" +__version__=''' $Id$ ''' + +from reportlab.lib import colors +from reportlab.lib.validators import * +from reportlab.lib.attrmap import * +from reportlab.graphics.shapes import Line, Rect, Polygon, Drawing, Group, String, Circle, Wedge +from reportlab.graphics.widgetbase import Widget +from reportlab.graphics import renderPDF +from signsandsymbols import _Symbol +import copy +from math import sin, cos, pi + +validFlag=OneOf(None, + 'UK', + 'USA', + 'Afghanistan', + 'Austria', + 'Belgium', + 'China', + 'Cuba', + 'Denmark', + 'Finland', + 'France', + 'Germany', + 'Greece', + 'Ireland', + 'Italy', + 'Japan', + 'Luxembourg', + 'Holland', + 'Palestine', + 'Portugal', + 'Russia', + 'Spain', + 'Sweden', + 'Norway', + 'CzechRepublic', + 'Turkey', + 'Switzerland', + 'EU', + 'Brazil' + ) + +_size = 100. + +class Star(_Symbol): + """This draws a 5-pointed star. + + possible attributes: + 'x', 'y', 'size', 'fillColor', 'strokeColor' + + """ + _attrMap = AttrMap(BASE=_Symbol, + angle = AttrMapValue(isNumber, desc='angle in degrees'), + ) + _size = 100. + + def __init__(self): + _Symbol.__init__(self) + self.size = 100 + self.fillColor = colors.yellow + self.strokeColor = None + self.angle = 0 + + def demo(self): + D = Drawing(200, 100) + et = Star() + et.x=50 + et.y=0 + D.add(et) + labelFontSize = 10 + D.add(String(et.x+(et.size/2.0),(et.y-(1.2*labelFontSize)), + et.__class__.__name__, fillColor=colors.black, textAnchor='middle', + fontSize=labelFontSize)) + return D + + def draw(self): + s = float(self.size) #abbreviate as we will use this a lot + g = Group() + + # new algorithm from markers.StarFive + R = float(self.size)/2 + r = R*sin(18*(pi/180.0))/cos(36*(pi/180.0)) + P = [] + angle = 90 + for i in xrange(5): + for radius in R, r: + theta = angle*(pi/180.0) + P.append(radius*cos(theta)) + P.append(radius*sin(theta)) + angle = angle + 36 + # star specific bits + star = Polygon(P, + fillColor = self.fillColor, + strokeColor = self.strokeColor, + strokeWidth=s/50) + g.rotate(self.angle) + g.shift(self.x+self.dx,self.y+self.dy) + g.add(star) + + return g + +class Flag(_Symbol): + """This is a generic flag class that all the flags in this file use as a basis. + + This class basically provides edges and a tidy-up routine to hide any bits of + line that overlap the 'outside' of the flag + + possible attributes: + 'x', 'y', 'size', 'fillColor' + """ + + _attrMap = AttrMap(BASE=_Symbol, + fillColor = AttrMapValue(isColor, desc='Background color'), + border = AttrMapValue(isBoolean, 'Whether a background is drawn'), + kind = AttrMapValue(validFlag, desc='Which flag'), + ) + + _cache = {} + + def __init__(self,**kw): + _Symbol.__init__(self) + self.kind = None + self.size = 100 + self.fillColor = colors.white + self.border=1 + self.setProperties(kw) + + def availableFlagNames(self): + '''return a list of the things we can display''' + return filter(lambda x: x is not None, self._attrMap['kind'].validate._enum) + + def _Flag_None(self): + s = _size # abbreviate as we will use this a lot + g = Group() + g.add(Rect(0, 0, s*2, s, fillColor = colors.purple, strokeColor = colors.black, strokeWidth=0)) + return g + + def _borderDraw(self,f): + s = self.size # abbreviate as we will use this a lot + g = Group() + g.add(f) + x, y, sW = self.x+self.dx, self.y+self.dy, self.strokeWidth/2. + g.insert(0,Rect(-sW, -sW, width=getattr(self,'_width',2*s)+3*sW, height=getattr(self,'_height',s)+2*sW, + fillColor = None, strokeColor = self.strokeColor, strokeWidth=sW*2)) + g.shift(x,y) + g.scale(s/_size, s/_size) + return g + + def draw(self): + kind = self.kind or 'None' + f = self._cache.get(kind) + if not f: + f = getattr(self,'_Flag_'+kind)() + self._cache[kind] = f._explode() + return self._borderDraw(f) + + def clone(self): + return copy.copy(self) + + def demo(self): + D = Drawing(200, 100) + name = self.availableFlagNames() + import time + name = name[int(time.time()) % len(name)] + fx = Flag() + fx.kind = name + fx.x = 0 + fx.y = 0 + D.add(fx) + labelFontSize = 10 + D.add(String(fx.x+(fx.size/2),(fx.y-(1.2*labelFontSize)), + name, fillColor=colors.black, textAnchor='middle', + fontSize=labelFontSize)) + labelFontSize = int(fx.size/4) + D.add(String(fx.x+(fx.size),(fx.y+((fx.size/2))), + "SAMPLE", fillColor=colors.gold, textAnchor='middle', + fontSize=labelFontSize, fontName="Helvetica-Bold")) + return D + + def _Flag_UK(self): + s = _size + g = Group() + w = s*2 + g.add(Rect(0, 0, w, s, fillColor = colors.navy, strokeColor = colors.black, strokeWidth=0)) + g.add(Polygon([0,0, s*.225,0, w,s*(1-.1125), w,s, w-s*.225,s, 0, s*.1125], fillColor = colors.mintcream, strokeColor=None, strokeWidth=0)) + g.add(Polygon([0,s*(1-.1125), 0, s, s*.225,s, w, s*.1125, w,0, w-s*.225,0], fillColor = colors.mintcream, strokeColor=None, strokeWidth=0)) + g.add(Polygon([0, s-(s/15), (s-((s/10)*4)), (s*0.65), (s-(s/10)*3), (s*0.65), 0, s], fillColor = colors.red, strokeColor = None, strokeWidth=0)) + g.add(Polygon([0, 0, (s-((s/10)*3)), (s*0.35), (s-((s/10)*2)), (s*0.35), (s/10), 0], fillColor = colors.red, strokeColor = None, strokeWidth=0)) + g.add(Polygon([w, s, (s+((s/10)*3)), (s*0.65), (s+((s/10)*2)), (s*0.65), w-(s/10), s], fillColor = colors.red, strokeColor = None, strokeWidth=0)) + g.add(Polygon([w, (s/15), (s+((s/10)*4)), (s*0.35), (s+((s/10)*3)), (s*0.35), w, 0], fillColor = colors.red, strokeColor = None, strokeWidth=0)) + g.add(Rect(((s*0.42)*2), 0, width=(0.16*s)*2, height=s, fillColor = colors.mintcream, strokeColor = None, strokeWidth=0)) + g.add(Rect(0, (s*0.35), width=w, height=s*0.3, fillColor = colors.mintcream, strokeColor = None, strokeWidth=0)) + g.add(Rect(((s*0.45)*2), 0, width=(0.1*s)*2, height=s, fillColor = colors.red, strokeColor = None, strokeWidth=0)) + g.add(Rect(0, (s*0.4), width=w, height=s*0.2, fillColor = colors.red, strokeColor = None, strokeWidth=0)) + return g + + def _Flag_USA(self): + s = _size # abbreviate as we will use this a lot + g = Group() + + box = Rect(0, 0, s*2, s, fillColor = colors.mintcream, strokeColor = colors.black, strokeWidth=0) + g.add(box) + + for stripecounter in range (13,0, -1): + stripeheight = s/13.0 + if not (stripecounter%2 == 0): + stripecolor = colors.red + else: + stripecolor = colors.mintcream + redorwhiteline = Rect(0, (s-(stripeheight*stripecounter)), width=s*2, height=stripeheight, + fillColor = stripecolor, strokeColor = None, strokeWidth=20) + g.add(redorwhiteline) + + bluebox = Rect(0, (s-(stripeheight*7)), width=0.8*s, height=stripeheight*7, + fillColor = colors.darkblue, strokeColor = None, strokeWidth=0) + g.add(bluebox) + + lss = s*0.045 + lss2 = lss/2 + s9 = s/9 + s7 = s/7 + for starxcounter in range(5): + for starycounter in range(4): + ls = Star() + ls.size = lss + ls.x = 0-s/22+lss/2+s7+starxcounter*s7 + ls.fillColor = colors.mintcream + ls.y = s-(starycounter+1)*s9+lss2 + g.add(ls) + + for starxcounter in range(6): + for starycounter in range(5): + ls = Star() + ls.size = lss + ls.x = 0-(s/22)+lss/2+s/14+starxcounter*s7 + ls.fillColor = colors.mintcream + ls.y = s-(starycounter+1)*s9+(s/18)+lss2 + g.add(ls) + return g + + def _Flag_Afghanistan(self): + s = _size + g = Group() + + box = Rect(0, 0, s*2, s, + fillColor = colors.mintcream, strokeColor = colors.black, strokeWidth=0) + g.add(box) + + greenbox = Rect(0, ((s/3.0)*2.0), width=s*2.0, height=s/3.0, + fillColor = colors.limegreen, strokeColor = None, strokeWidth=0) + g.add(greenbox) + + blackbox = Rect(0, 0, width=s*2.0, height=s/3.0, + fillColor = colors.black, strokeColor = None, strokeWidth=0) + g.add(blackbox) + return g + + def _Flag_Austria(self): + s = _size # abbreviate as we will use this a lot + g = Group() + + box = Rect(0, 0, s*2, s, fillColor = colors.mintcream, + strokeColor = colors.black, strokeWidth=0) + g.add(box) + + + redbox1 = Rect(0, 0, width=s*2.0, height=s/3.0, + fillColor = colors.red, strokeColor = None, strokeWidth=0) + g.add(redbox1) + + redbox2 = Rect(0, ((s/3.0)*2.0), width=s*2.0, height=s/3.0, + fillColor = colors.red, strokeColor = None, strokeWidth=0) + g.add(redbox2) + return g + + def _Flag_Belgium(self): + s = _size + g = Group() + + box = Rect(0, 0, s*2, s, + fillColor = colors.black, strokeColor = colors.black, strokeWidth=0) + g.add(box) + + + box1 = Rect(0, 0, width=(s/3.0)*2.0, height=s, + fillColor = colors.black, strokeColor = None, strokeWidth=0) + g.add(box1) + + box2 = Rect(((s/3.0)*2.0), 0, width=(s/3.0)*2.0, height=s, + fillColor = colors.gold, strokeColor = None, strokeWidth=0) + g.add(box2) + + box3 = Rect(((s/3.0)*4.0), 0, width=(s/3.0)*2.0, height=s, + fillColor = colors.red, strokeColor = None, strokeWidth=0) + g.add(box3) + return g + + def _Flag_China(self): + s = _size + g = Group() + self._width = w = s*1.5 + g.add(Rect(0, 0, w, s, fillColor=colors.red, strokeColor=None, strokeWidth=0)) + + def addStar(x,y,size,angle,g=g,w=s/20,x0=0,y0=s/2): + s = Star() + s.fillColor=colors.yellow + s.angle = angle + s.size = size*w*2 + s.x = x*w+x0 + s.y = y*w+y0 + g.add(s) + + addStar(5,5,3, 0) + addStar(10,1,1,36.86989765) + addStar(12,3,1,8.213210702) + addStar(12,6,1,16.60154960) + addStar(10,8,1,53.13010235) + return g + + def _Flag_Cuba(self): + s = _size + g = Group() + + for i in range(5): + stripe = Rect(0, i*s/5, width=s*2, height=s/5, + fillColor = [colors.darkblue, colors.mintcream][i%2], + strokeColor = None, + strokeWidth=0) + g.add(stripe) + + redwedge = Polygon(points = [ 0, 0, 4*s/5, (s/2), 0, s], + fillColor = colors.red, strokeColor = None, strokeWidth=0) + g.add(redwedge) + + star = Star() + star.x = 2.5*s/10 + star.y = s/2 + star.size = 3*s/10 + star.fillColor = colors.white + g.add(star) + + box = Rect(0, 0, s*2, s, + fillColor = None, + strokeColor = colors.black, + strokeWidth=0) + g.add(box) + + return g + + def _Flag_Denmark(self): + s = _size + g = Group() + self._width = w = s*1.4 + + box = Rect(0, 0, w, s, + fillColor = colors.red, strokeColor = colors.black, strokeWidth=0) + g.add(box) + + whitebox1 = Rect(((s/5)*2), 0, width=s/6, height=s, + fillColor = colors.mintcream, strokeColor = None, strokeWidth=0) + g.add(whitebox1) + + whitebox2 = Rect(0, ((s/2)-(s/12)), width=w, height=s/6, + fillColor = colors.mintcream, strokeColor = None, strokeWidth=0) + g.add(whitebox2) + return g + + def _Flag_Finland(self): + s = _size + g = Group() + + # crossbox specific bits + box = Rect(0, 0, s*2, s, + fillColor = colors.ghostwhite, strokeColor = colors.black, strokeWidth=0) + g.add(box) + + blueline1 = Rect((s*0.6), 0, width=0.3*s, height=s, + fillColor = colors.darkblue, strokeColor = None, strokeWidth=0) + g.add(blueline1) + + blueline2 = Rect(0, (s*0.4), width=s*2, height=s*0.3, + fillColor = colors.darkblue, strokeColor = None, strokeWidth=0) + g.add(blueline2) + return g + + def _Flag_France(self): + s = _size + g = Group() + + box = Rect(0, 0, s*2, s, fillColor = colors.navy, strokeColor = colors.black, strokeWidth=0) + g.add(box) + + bluebox = Rect(0, 0, width=((s/3.0)*2.0), height=s, + fillColor = colors.blue, strokeColor = None, strokeWidth=0) + g.add(bluebox) + + whitebox = Rect(((s/3.0)*2.0), 0, width=((s/3.0)*2.0), height=s, + fillColor = colors.mintcream, strokeColor = None, strokeWidth=0) + g.add(whitebox) + + redbox = Rect(((s/3.0)*4.0), 0, width=((s/3.0)*2.0), height=s, + fillColor = colors.red, + strokeColor = None, + strokeWidth=0) + g.add(redbox) + return g + + def _Flag_Germany(self): + s = _size + g = Group() + + box = Rect(0, 0, s*2, s, + fillColor = colors.gold, strokeColor = colors.black, strokeWidth=0) + g.add(box) + + blackbox1 = Rect(0, ((s/3.0)*2.0), width=s*2.0, height=s/3.0, + fillColor = colors.black, strokeColor = None, strokeWidth=0) + g.add(blackbox1) + + redbox1 = Rect(0, (s/3.0), width=s*2.0, height=s/3.0, + fillColor = colors.orangered, strokeColor = None, strokeWidth=0) + g.add(redbox1) + return g + + def _Flag_Greece(self): + s = _size + g = Group() + + box = Rect(0, 0, s*2, s, fillColor = colors.gold, + strokeColor = colors.black, strokeWidth=0) + g.add(box) + + for stripecounter in range (9,0, -1): + stripeheight = s/9.0 + if not (stripecounter%2 == 0): + stripecolor = colors.deepskyblue + else: + stripecolor = colors.mintcream + + blueorwhiteline = Rect(0, (s-(stripeheight*stripecounter)), width=s*2, height=stripeheight, + fillColor = stripecolor, strokeColor = None, strokeWidth=20) + g.add(blueorwhiteline) + + bluebox1 = Rect(0, ((s)-stripeheight*5), width=(stripeheight*5), height=stripeheight*5, + fillColor = colors.deepskyblue, strokeColor = None, strokeWidth=0) + g.add(bluebox1) + + whiteline1 = Rect(0, ((s)-stripeheight*3), width=stripeheight*5, height=stripeheight, + fillColor = colors.mintcream, strokeColor = None, strokeWidth=0) + g.add(whiteline1) + + whiteline2 = Rect((stripeheight*2), ((s)-stripeheight*5), width=stripeheight, height=stripeheight*5, + fillColor = colors.mintcream, strokeColor = None, strokeWidth=0) + g.add(whiteline2) + + return g + + def _Flag_Ireland(self): + s = _size + g = Group() + + box = Rect(0, 0, s*2, s, + fillColor = colors.forestgreen, strokeColor = colors.black, strokeWidth=0) + g.add(box) + + whitebox = Rect(((s*2.0)/3.0), 0, width=(2.0*(s*2.0)/3.0), height=s, + fillColor = colors.mintcream, strokeColor = None, strokeWidth=0) + g.add(whitebox) + + orangebox = Rect(((2.0*(s*2.0)/3.0)), 0, width=(s*2.0)/3.0, height=s, + fillColor = colors.darkorange, strokeColor = None, strokeWidth=0) + g.add(orangebox) + return g + + def _Flag_Italy(self): + s = _size + g = Group() + g.add(Rect(0,0,s*2,s,fillColor=colors.forestgreen,strokeColor=None, strokeWidth=0)) + g.add(Rect((2*s)/3, 0, width=(s*4)/3, height=s, fillColor = colors.mintcream, strokeColor = None, strokeWidth=0)) + g.add(Rect((4*s)/3, 0, width=(s*2)/3, height=s, fillColor = colors.red, strokeColor = None, strokeWidth=0)) + return g + + def _Flag_Japan(self): + s = _size + g = Group() + w = self._width = s*1.5 + g.add(Rect(0,0,w,s,fillColor=colors.mintcream,strokeColor=None, strokeWidth=0)) + g.add(Circle(cx=w/2,cy=s/2,r=0.3*w,fillColor=colors.red,strokeColor=None, strokeWidth=0)) + return g + + def _Flag_Luxembourg(self): + s = _size + g = Group() + + box = Rect(0, 0, s*2, s, + fillColor = colors.mintcream, strokeColor = colors.black, strokeWidth=0) + g.add(box) + + redbox = Rect(0, ((s/3.0)*2.0), width=s*2.0, height=s/3.0, + fillColor = colors.red, strokeColor = None, strokeWidth=0) + g.add(redbox) + + bluebox = Rect(0, 0, width=s*2.0, height=s/3.0, + fillColor = colors.dodgerblue, strokeColor = None, strokeWidth=0) + g.add(bluebox) + return g + + def _Flag_Holland(self): + s = _size + g = Group() + + box = Rect(0, 0, s*2, s, + fillColor = colors.mintcream, strokeColor = colors.black, strokeWidth=0) + g.add(box) + + redbox = Rect(0, ((s/3.0)*2.0), width=s*2.0, height=s/3.0, + fillColor = colors.red, strokeColor = None, strokeWidth=0) + g.add(redbox) + + bluebox = Rect(0, 0, width=s*2.0, height=s/3.0, + fillColor = colors.darkblue, strokeColor = None, strokeWidth=0) + g.add(bluebox) + return g + + def _Flag_Portugal(self): + return Group() + + def _Flag_Russia(self): + s = _size + g = Group() + w = self._width = s*1.5 + t = s/3 + g.add(Rect(0, 0, width=w, height=t, fillColor = colors.red, strokeColor = None, strokeWidth=0)) + g.add(Rect(0, t, width=w, height=t, fillColor = colors.blue, strokeColor = None, strokeWidth=0)) + g.add(Rect(0, 2*t, width=w, height=t, fillColor = colors.mintcream, strokeColor = None, strokeWidth=0)) + return g + + def _Flag_Spain(self): + s = _size + g = Group() + w = self._width = s*1.5 + g.add(Rect(0, 0, width=w, height=s, fillColor = colors.red, strokeColor = None, strokeWidth=0)) + g.add(Rect(0, (s/4), width=w, height=s/2, fillColor = colors.yellow, strokeColor = None, strokeWidth=0)) + return g + + def _Flag_Sweden(self): + s = _size + g = Group() + self._width = s*1.4 + box = Rect(0, 0, self._width, s, + fillColor = colors.dodgerblue, strokeColor = colors.black, strokeWidth=0) + g.add(box) + + box1 = Rect(((s/5)*2), 0, width=s/6, height=s, + fillColor = colors.gold, strokeColor = None, strokeWidth=0) + g.add(box1) + + box2 = Rect(0, ((s/2)-(s/12)), width=self._width, height=s/6, + fillColor = colors.gold, + strokeColor = None, + strokeWidth=0) + g.add(box2) + return g + + def _Flag_Norway(self): + s = _size + g = Group() + self._width = s*1.4 + + box = Rect(0, 0, self._width, s, + fillColor = colors.red, strokeColor = colors.black, strokeWidth=0) + g.add(box) + + box = Rect(0, 0, self._width, s, + fillColor = colors.red, strokeColor = colors.black, strokeWidth=0) + g.add(box) + + whiteline1 = Rect(((s*0.2)*2), 0, width=s*0.2, height=s, + fillColor = colors.ghostwhite, strokeColor = None, strokeWidth=0) + g.add(whiteline1) + + whiteline2 = Rect(0, (s*0.4), width=self._width, height=s*0.2, + fillColor = colors.ghostwhite, strokeColor = None, strokeWidth=0) + g.add(whiteline2) + + blueline1 = Rect(((s*0.225)*2), 0, width=0.1*s, height=s, + fillColor = colors.darkblue, strokeColor = None, strokeWidth=0) + g.add(blueline1) + + blueline2 = Rect(0, (s*0.45), width=self._width, height=s*0.1, + fillColor = colors.darkblue, strokeColor = None, strokeWidth=0) + g.add(blueline2) + return g + + def _Flag_CzechRepublic(self): + s = _size + g = Group() + box = Rect(0, 0, s*2, s, + fillColor = colors.mintcream, + strokeColor = colors.black, + strokeWidth=0) + g.add(box) + + redbox = Rect(0, 0, width=s*2, height=s/2, + fillColor = colors.red, + strokeColor = None, + strokeWidth=0) + g.add(redbox) + + bluewedge = Polygon(points = [ 0, 0, s, (s/2), 0, s], + fillColor = colors.darkblue, strokeColor = None, strokeWidth=0) + g.add(bluewedge) + return g + + def _Flag_Palestine(self): + s = _size + g = Group() + box = Rect(0, s/3, s*2, s/3, + fillColor = colors.mintcream, + strokeColor = None, + strokeWidth=0) + g.add(box) + + greenbox = Rect(0, 0, width=s*2, height=s/3, + fillColor = colors.limegreen, + strokeColor = None, + strokeWidth=0) + g.add(greenbox) + + blackbox = Rect(0, 2*s/3, width=s*2, height=s/3, + fillColor = colors.black, + strokeColor = None, + strokeWidth=0) + g.add(blackbox) + + redwedge = Polygon(points = [ 0, 0, 2*s/3, (s/2), 0, s], + fillColor = colors.red, strokeColor = None, strokeWidth=0) + g.add(redwedge) + return g + + def _Flag_Turkey(self): + s = _size + g = Group() + + box = Rect(0, 0, s*2, s, + fillColor = colors.red, + strokeColor = colors.black, + strokeWidth=0) + g.add(box) + + whitecircle = Circle(cx=((s*0.35)*2), cy=s/2, r=s*0.3, + fillColor = colors.mintcream, + strokeColor = None, + strokeWidth=0) + g.add(whitecircle) + + redcircle = Circle(cx=((s*0.39)*2), cy=s/2, r=s*0.24, + fillColor = colors.red, + strokeColor = None, + strokeWidth=0) + g.add(redcircle) + + ws = Star() + ws.angle = 15 + ws.size = s/5 + ws.x = (s*0.5)*2+ws.size/2 + ws.y = (s*0.5) + ws.fillColor = colors.mintcream + ws.strokeColor = None + g.add(ws) + return g + + def _Flag_Switzerland(self): + s = _size + g = Group() + self._width = s + + g.add(Rect(0, 0, s, s, fillColor = colors.red, strokeColor = colors.black, strokeWidth=0)) + g.add(Line((s/2), (s/5.5), (s/2), (s-(s/5.5)), + fillColor = colors.mintcream, strokeColor = colors.mintcream, strokeWidth=(s/5))) + g.add(Line((s/5.5), (s/2), (s-(s/5.5)), (s/2), + fillColor = colors.mintcream, strokeColor = colors.mintcream, strokeWidth=s/5)) + return g + + def _Flag_EU(self): + s = _size + g = Group() + w = self._width = 1.5*s + + g.add(Rect(0, 0, w, s, fillColor = colors.darkblue, strokeColor = None, strokeWidth=0)) + centerx=w/2 + centery=s/2 + radius=s/3 + yradius = radius + xradius = radius + nStars = 12 + delta = 2*pi/nStars + for i in range(nStars): + rad = i*delta + gs = Star() + gs.x=cos(rad)*radius+centerx + gs.y=sin(rad)*radius+centery + gs.size=s/10 + gs.fillColor=colors.gold + g.add(gs) + return g + + def _Flag_Brazil(self): + s = _size # abbreviate as we will use this a lot + g = Group() + + m = s/14 + self._width = w = (m * 20) + + def addStar(x,y,size, g=g, w=w, s=s, m=m): + st = Star() + st.fillColor=colors.mintcream + st.size = size*m + st.x = (w/2) + (x * (0.35 * m)) + st.y = (s/2) + (y * (0.35 * m)) + g.add(st) + + g.add(Rect(0, 0, w, s, fillColor = colors.green, strokeColor = None, strokeWidth=0)) + g.add(Polygon(points = [ 1.7*m, (s/2), (w/2), s-(1.7*m), w-(1.7*m),(s/2),(w/2), 1.7*m], + fillColor = colors.yellow, strokeColor = None, strokeWidth=0)) + g.add(Circle(cx=w/2, cy=s/2, r=3.5*m, + fillColor=colors.blue,strokeColor=None, strokeWidth=0)) + g.add(Wedge((w/2)-(2*m), 0, 8.5*m, 50, 98.1, 8.5*m, + fillColor=colors.mintcream,strokeColor=None, strokeWidth=0)) + g.add(Wedge((w/2), (s/2), 3.501*m, 156, 352, 3.501*m, + fillColor=colors.mintcream,strokeColor=None, strokeWidth=0)) + g.add(Wedge((w/2)-(2*m), 0, 8*m, 48.1, 100, 8*m, + fillColor=colors.blue,strokeColor=None, strokeWidth=0)) + g.add(Rect(0, 0, w, (s/4) + 1.7*m, + fillColor = colors.green, strokeColor = None, strokeWidth=0)) + g.add(Polygon(points = [ 1.7*m,(s/2), (w/2),s/2 - 2*m, w-(1.7*m),(s/2) , (w/2),1.7*m], + fillColor = colors.yellow, strokeColor = None, strokeWidth=0)) + g.add(Wedge(w/2, s/2, 3.502*m, 166, 342.1, 3.502*m, + fillColor=colors.blue,strokeColor=None, strokeWidth=0)) + + addStar(3.2,3.5,0.3) + addStar(-8.5,1.5,0.3) + addStar(-7.5,-3,0.3) + addStar(-4,-5.5,0.3) + addStar(0,-4.5,0.3) + addStar(7,-3.5,0.3) + addStar(-3.5,-0.5,0.25) + addStar(0,-1.5,0.25) + addStar(1,-2.5,0.25) + addStar(3,-7,0.25) + addStar(5,-6.5,0.25) + addStar(6.5,-5,0.25) + addStar(7,-4.5,0.25) + addStar(-5.5,-3.2,0.25) + addStar(-6,-4.2,0.25) + addStar(-1,-2.75,0.2) + addStar(2,-5.5,0.2) + addStar(4,-5.5,0.2) + addStar(5,-7.5,0.2) + addStar(5,-5.5,0.2) + addStar(6,-5.5,0.2) + addStar(-8.8,-3.2,0.2) + addStar(2.5,0.5,0.2) + addStar(-0.2,-3.2,0.14) + addStar(-7.2,-2,0.14) + addStar(0,-8,0.1) + + sTmp = "ORDEM E PROGRESSO" + nTmp = len(sTmp) + delta = 0.850848010347/nTmp + radius = 7.9 *m + centerx = (w/2)-(2*m) + centery = 0 + for i in range(nTmp): + rad = 2*pi - i*delta -4.60766922527 + x=cos(rad)*radius+centerx + y=sin(rad)*radius+centery + if i == 6: + z = 0.35*m + else: + z= 0.45*m + g2 = Group(String(x, y, sTmp[i], fontName='Helvetica-Bold', + fontSize = z,strokeColor=None,fillColor=colors.green)) + g2.rotate(rad) + g.add(g2) + return g + +def makeFlag(name): + flag = Flag() + flag.kind = name + return flag + +def test(): + """This function produces three pdf files with examples of all the signs and symbols from this file. + """ +# page 1 + + labelFontSize = 10 + + X = (20,245) + + flags = [ + 'UK', + 'USA', + 'Afghanistan', + 'Austria', + 'Belgium', + 'Denmark', + 'Cuba', + 'Finland', + 'France', + 'Germany', + 'Greece', + 'Ireland', + 'Italy', + 'Luxembourg', + 'Holland', + 'Palestine', + 'Portugal', + 'Spain', + 'Sweden', + 'Norway', + 'CzechRepublic', + 'Turkey', + 'Switzerland', + 'EU', + 'Brazil', + ] + y = Y0 = 530 + f = 0 + D = None + for name in flags: + if not D: D = Drawing(450,650) + flag = makeFlag(name) + i = flags.index(name) + flag.x = X[i%2] + flag.y = y + D.add(flag) + D.add(String(flag.x+(flag.size/2),(flag.y-(1.2*labelFontSize)), + name, fillColor=colors.black, textAnchor='middle', fontSize=labelFontSize)) + if i%2: y = y - 125 + if (i%2 and y<0) or name==flags[-1]: + renderPDF.drawToFile(D, 'flags%02d.pdf'%f, 'flags.py - Page #%d'%(f+1)) + y = Y0 + f = f+1 + D = None + +if __name__=='__main__': + test() diff --git a/bin/reportlab/graphics/widgets/grids.py b/bin/reportlab/graphics/widgets/grids.py new file mode 100644 index 00000000000..ff859285907 --- /dev/null +++ b/bin/reportlab/graphics/widgets/grids.py @@ -0,0 +1,504 @@ +#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/widgets/grids.py +__version__=''' $Id$ ''' + +from reportlab.lib import colors +from reportlab.lib.validators import isNumber, isColorOrNone, isBoolean, isListOfNumbers, OneOf, isListOfColors +from reportlab.lib.attrmap import AttrMap, AttrMapValue +from reportlab.graphics.shapes import Drawing, Group, Line, Rect, LineShape, definePath, EmptyClipPath +from reportlab.graphics.widgetbase import Widget + +def frange(start, end=None, inc=None): + "A range function, that does accept float increments..." + + if end == None: + end = start + 0.0 + start = 0.0 + + if inc == None: + inc = 1.0 + + L = [] + end = end - inc*0.0001 #to avoid numrical problems + while 1: + next = start + len(L) * inc + if inc > 0 and next >= end: + break + elif inc < 0 and next <= end: + break + L.append(next) + + return L + + +def makeDistancesList(list): + """Returns a list of distances between adjacent numbers in some input list. + + E.g. [1, 1, 2, 3, 5, 7] -> [0, 1, 1, 2, 2] + """ + + d = [] + for i in range(len(list[:-1])): + d.append(list[i+1] - list[i]) + + return d + + +class Grid(Widget): + """This makes a rectangular grid of equidistant stripes. + + The grid contains an outer border rectangle, and stripes + inside which can be drawn with lines and/or as solid tiles. + The drawing order is: outer rectangle, then lines and tiles. + + The stripes' width is indicated as 'delta'. The sequence of + stripes can have an offset named 'delta0'. Both values need + to be positive! + """ + + _attrMap = AttrMap( + x = AttrMapValue(isNumber, desc="The grid's lower-left x position."), + y = AttrMapValue(isNumber, desc="The grid's lower-left y position."), + width = AttrMapValue(isNumber, desc="The grid's width."), + height = AttrMapValue(isNumber, desc="The grid's height."), + orientation = AttrMapValue(OneOf(('vertical', 'horizontal')), + desc='Determines if stripes are vertical or horizontal.'), + useLines = AttrMapValue(OneOf((0, 1)), + desc='Determines if stripes are drawn with lines.'), + useRects = AttrMapValue(OneOf((0, 1)), + desc='Determines if stripes are drawn with solid rectangles.'), + delta = AttrMapValue(isNumber, + desc='Determines the width/height of the stripes.'), + delta0 = AttrMapValue(isNumber, + desc='Determines the stripes initial width/height offset.'), + deltaSteps = AttrMapValue(isListOfNumbers, + desc='List of deltas to be used cyclically.'), + stripeColors = AttrMapValue(isListOfColors, + desc='Colors applied cyclically in the right or upper direction.'), + fillColor = AttrMapValue(isColorOrNone, + desc='Background color for entire rectangle.'), + strokeColor = AttrMapValue(isColorOrNone, + desc='Color used for lines.'), + strokeWidth = AttrMapValue(isNumber, + desc='Width used for lines.'), + rectStrokeColor = AttrMapValue(isColorOrNone, desc='Color for outer rect stroke.'), + rectStrokeWidth = AttrMapValue(isColorOrNone, desc='Width for outer rect stroke.'), + ) + + def __init__(self): + self.x = 0 + self.y = 0 + self.width = 100 + self.height = 100 + self.orientation = 'vertical' + self.useLines = 0 + self.useRects = 1 + self.delta = 20 + self.delta0 = 0 + self.deltaSteps = [] + self.fillColor = colors.white + self.stripeColors = [colors.red, colors.green, colors.blue] + self.strokeColor = colors.black + self.strokeWidth = 2 + + + def demo(self): + D = Drawing(100, 100) + + g = Grid() + D.add(g) + + return D + + def makeOuterRect(self): + strokeColor = getattr(self,'rectStrokeColor',self.strokeColor) + strokeWidth = getattr(self,'rectStrokeWidth',self.strokeWidth) + if self.fillColor or (strokeColor and strokeWidth): + rect = Rect(self.x, self.y, self.width, self.height) + rect.fillColor = self.fillColor + rect.strokeColor = strokeColor + rect.strokeWidth = strokeWidth + return rect + else: + return None + + def makeLinePosList(self, start, isX=0): + "Returns a list of positions where to place lines." + + w, h = self.width, self.height + if isX: + length = w + else: + length = h + if self.deltaSteps: + r = [start + self.delta0] + i = 0 + while 1: + if r[-1] > start + length: + del r[-1] + break + r.append(r[-1] + self.deltaSteps[i % len(self.deltaSteps)]) + i = i + 1 + else: + r = frange(start + self.delta0, start + length, self.delta) + + r.append(start + length) + if self.delta0 != 0: + r.insert(0, start) + #print 'Grid.makeLinePosList() -> %s' % r + return r + + + def makeInnerLines(self): + # inner grid lines + group = Group() + + w, h = self.width, self.height + + if self.useLines == 1: + if self.orientation == 'vertical': + r = self.makeLinePosList(self.x, isX=1) + for x in r: + line = Line(x, self.y, x, self.y + h) + line.strokeColor = self.strokeColor + line.strokeWidth = self.strokeWidth + group.add(line) + elif self.orientation == 'horizontal': + r = self.makeLinePosList(self.y, isX=0) + for y in r: + line = Line(self.x, y, self.x + w, y) + line.strokeColor = self.strokeColor + line.strokeWidth = self.strokeWidth + group.add(line) + + return group + + + def makeInnerTiles(self): + # inner grid lines + group = Group() + + w, h = self.width, self.height + + # inner grid stripes (solid rectangles) + if self.useRects == 1: + cols = self.stripeColors + + if self.orientation == 'vertical': + r = self.makeLinePosList(self.x, isX=1) + elif self.orientation == 'horizontal': + r = self.makeLinePosList(self.y, isX=0) + + dist = makeDistancesList(r) + + i = 0 + for j in range(len(dist)): + if self.orientation == 'vertical': + x = r[j] + stripe = Rect(x, self.y, dist[j], h) + elif self.orientation == 'horizontal': + y = r[j] + stripe = Rect(self.x, y, w, dist[j]) + stripe.fillColor = cols[i % len(cols)] + stripe.strokeColor = None + group.add(stripe) + i = i + 1 + + return group + + + def draw(self): + # general widget bits + group = Group() + + group.add(self.makeOuterRect()) + group.add(self.makeInnerTiles()) + group.add(self.makeInnerLines(),name='_gridLines') + + return group + + +class DoubleGrid(Widget): + """This combines two ordinary Grid objects orthogonal to each other. + """ + + _attrMap = AttrMap( + x = AttrMapValue(isNumber, desc="The grid's lower-left x position."), + y = AttrMapValue(isNumber, desc="The grid's lower-left y position."), + width = AttrMapValue(isNumber, desc="The grid's width."), + height = AttrMapValue(isNumber, desc="The grid's height."), + grid0 = AttrMapValue(None, desc="The first grid component."), + grid1 = AttrMapValue(None, desc="The second grid component."), + ) + + def __init__(self): + self.x = 0 + self.y = 0 + self.width = 100 + self.height = 100 + + g0 = Grid() + g0.x = self.x + g0.y = self.y + g0.width = self.width + g0.height = self.height + g0.orientation = 'vertical' + g0.useLines = 1 + g0.useRects = 0 + g0.delta = 20 + g0.delta0 = 0 + g0.deltaSteps = [] + g0.fillColor = colors.white + g0.stripeColors = [colors.red, colors.green, colors.blue] + g0.strokeColor = colors.black + g0.strokeWidth = 1 + + g1 = Grid() + g1.x = self.x + g1.y = self.y + g1.width = self.width + g1.height = self.height + g1.orientation = 'horizontal' + g1.useLines = 1 + g1.useRects = 0 + g1.delta = 20 + g1.delta0 = 0 + g1.deltaSteps = [] + g1.fillColor = colors.white + g1.stripeColors = [colors.red, colors.green, colors.blue] + g1.strokeColor = colors.black + g1.strokeWidth = 1 + + self.grid0 = g0 + self.grid1 = g1 + + +## # This gives an AttributeError: +## # DoubleGrid instance has no attribute 'grid0' +## def __setattr__(self, name, value): +## if name in ('x', 'y', 'width', 'height'): +## setattr(self.grid0, name, value) +## setattr(self.grid1, name, value) + + + def demo(self): + D = Drawing(100, 100) + g = DoubleGrid() + D.add(g) + return D + + + def draw(self): + group = Group() + g0, g1 = self.grid0, self.grid1 + # Order groups to make sure both v and h lines + # are visible (works only when there is only + # one kind of stripes, v or h). + G = g0.useRects == 1 and g1.useRects == 0 and (g0,g1) or (g1,g0) + for g in G: + group.add(g.makeOuterRect()) + for g in G: + group.add(g.makeInnerTiles()) + group.add(g.makeInnerLines(),name='_gridLines') + + return group + + +class ShadedRect(Widget): + """This makes a rectangle with shaded colors between two colors. + + Colors are interpolated linearly between 'fillColorStart' + and 'fillColorEnd', both of which appear at the margins. + If 'numShades' is set to one, though, only 'fillColorStart' + is used. + """ + + _attrMap = AttrMap( + x = AttrMapValue(isNumber, desc="The grid's lower-left x position."), + y = AttrMapValue(isNumber, desc="The grid's lower-left y position."), + width = AttrMapValue(isNumber, desc="The grid's width."), + height = AttrMapValue(isNumber, desc="The grid's height."), + orientation = AttrMapValue(OneOf(('vertical', 'horizontal')), desc='Determines if stripes are vertical or horizontal.'), + numShades = AttrMapValue(isNumber, desc='The number of interpolating colors.'), + fillColorStart = AttrMapValue(isColorOrNone, desc='Start value of the color shade.'), + fillColorEnd = AttrMapValue(isColorOrNone, desc='End value of the color shade.'), + strokeColor = AttrMapValue(isColorOrNone, desc='Color used for border line.'), + strokeWidth = AttrMapValue(isNumber, desc='Width used for lines.'), + cylinderMode = AttrMapValue(isBoolean, desc='True if shading reverses in middle.'), + ) + + def __init__(self,**kw): + self.x = 0 + self.y = 0 + self.width = 100 + self.height = 100 + self.orientation = 'vertical' + self.numShades = 20 + self.fillColorStart = colors.pink + self.fillColorEnd = colors.black + self.strokeColor = colors.black + self.strokeWidth = 2 + self.cylinderMode = 0 + self.setProperties(kw) + + def demo(self): + D = Drawing(100, 100) + g = ShadedRect() + D.add(g) + + return D + + def _flipRectCorners(self): + "Flip rectangle's corners if width or height is negative." + x, y, width, height, fillColorStart, fillColorEnd = self.x, self.y, self.width, self.height, self.fillColorStart, self.fillColorEnd + if width < 0 and height > 0: + x = x + width + width = -width + if self.orientation=='vertical': fillColorStart, fillColorEnd = fillColorEnd, fillColorStart + elif height<0 and width>0: + y = y + height + height = -height + if self.orientation=='horizontal': fillColorStart, fillColorEnd = fillColorEnd, fillColorStart + elif height < 0 and height < 0: + x = x + width + width = -width + y = y + height + height = -height + return x, y, width, height, fillColorStart, fillColorEnd + + def draw(self): + # general widget bits + group = Group() + x, y, w, h, c0, c1 = self._flipRectCorners() + numShades = self.numShades + if self.cylinderMode: + if not numShades%2: numShades = numShades+1 + halfNumShades = (numShades-1)/2 + 1 + num = float(numShades) # must make it float! + vertical = self.orientation == 'vertical' + if vertical: + if numShades == 1: + V = [x] + else: + V = frange(x, x + w, w/num) + else: + if numShades == 1: + V = [y] + else: + V = frange(y, y + h, h/num) + + for v in V: + stripe = vertical and Rect(v, y, w/num, h) or Rect(x, v, w, h/num) + if self.cylinderMode: + if V.index(v)>=halfNumShades: + col = colors.linearlyInterpolatedColor(c1,c0,V[halfNumShades],V[-1], v) + else: + col = colors.linearlyInterpolatedColor(c0,c1,V[0],V[halfNumShades], v) + else: + col = colors.linearlyInterpolatedColor(c0,c1,V[0],V[-1], v) + stripe.fillColor = col + stripe.strokeColor = col + stripe.strokeWidth = 1 + group.add(stripe) + if self.strokeColor and self.strokeWidth>=0: + rect = Rect(x, y, w, h) + rect.strokeColor = self.strokeColor + rect.strokeWidth = self.strokeWidth + rect.fillColor = None + group.add(rect) + return group + + +def colorRange(c0, c1, n): + "Return a range of intermediate colors between c0 and c1" + if n==1: return [c0] + + C = [] + if n>1: + lim = n-1 + for i in range(n): + C.append(colors.linearlyInterpolatedColor(c0,c1,0,lim, i)) + return C + + +def centroid(P): + '''compute average point of a set of points''' + return reduce(lambda x,y, fn=float(len(P)): (x[0]+y[0]/fn,x[1]+y[1]/fn),P,(0,0)) + +def rotatedEnclosingRect(P, angle, rect): + ''' + given P a sequence P of x,y coordinate pairs and an angle in degrees + find the centroid of P and the axis at angle theta through it + find the extreme points of P wrt axis parallel distance and axis + orthogonal distance. Then compute the least rectangle that will still + enclose P when rotated by angle. + + The class R + ''' + from math import pi, cos, sin, tan + x0, y0 = centroid(P) + theta = (angle/180.)*pi + s,c=sin(theta),cos(theta) + def parallelAxisDist((x,y),s=s,c=c,x0=x0,y0=y0): + return (s*(y-y0)+c*(x-x0)) + def orthogonalAxisDist((x,y),s=s,c=c,x0=x0,y0=y0): + return (c*(y-y0)+s*(x-x0)) + L = map(parallelAxisDist,P) + L.sort() + a0, a1 = L[0], L[-1] + L = map(orthogonalAxisDist,P) + L.sort() + b0, b1 = L[0], L[-1] + rect.x, rect.width = a0, a1-a0 + rect.y, rect.height = b0, b1-b0 + g = Group(transform=(c,s,-s,c,x0,y0)) + g.add(rect) + return g + +class ShadedPolygon(Widget,LineShape): + _attrMap = AttrMap(BASE=LineShape, + angle = AttrMapValue(isNumber,desc="Shading angle"), + fillColorStart = AttrMapValue(isColorOrNone), + fillColorEnd = AttrMapValue(isColorOrNone), + numShades = AttrMapValue(isNumber, desc='The number of interpolating colors.'), + cylinderMode = AttrMapValue(isBoolean, desc='True if shading reverses in middle.'), + points = AttrMapValue(isListOfNumbers), + ) + + def __init__(self,**kw): + self.angle = 90 + self.fillColorStart = colors.red + self.fillColorEnd = colors.green + self.cylinderMode = 0 + self.numShades = 50 + self.points = [-1,-1,2,2,3,-1] + LineShape.__init__(self,kw) + + def draw(self): + P = self.points + P = map(lambda i, P=P:(P[i],P[i+1]),xrange(0,len(P),2)) + path = definePath([('moveTo',)+P[0]]+map(lambda x: ('lineTo',)+x,P[1:])+['closePath'], + fillColor=None, strokeColor=None) + path.isClipPath = 1 + g = Group() + g.add(path) + rect = ShadedRect(strokeWidth=0,strokeColor=None) + for k in 'fillColorStart', 'fillColorEnd', 'numShades', 'cylinderMode': + setattr(rect,k,getattr(self,k)) + g.add(rotatedEnclosingRect(P, self.angle, rect)) + g.add(EmptyClipPath) + path = path.copy() + path.isClipPath = 0 + path.strokeColor = self.strokeColor + path.strokeWidth = self.strokeWidth + g.add(path) + return g + +if __name__=='__main__': #noruntests + from reportlab.lib.colors import blue + from reportlab.graphics.shapes import Drawing + angle=45 + D = Drawing(120,120) + D.add(ShadedPolygon(points=(10,10,60,60,110,10),strokeColor=None,strokeWidth=1,angle=90,numShades=50,cylinderMode=0)) + D.save(formats=['gif'],fnRoot='shobj',outDir='/tmp') diff --git a/bin/reportlab/graphics/widgets/markers.py b/bin/reportlab/graphics/widgets/markers.py new file mode 100644 index 00000000000..4399dfe9a67 --- /dev/null +++ b/bin/reportlab/graphics/widgets/markers.py @@ -0,0 +1,228 @@ +#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/widgets/markers.py +""" +This modules defines a collection of markers used in charts. +""" +__version__=''' $Id$ ''' +from types import FunctionType, ClassType +from reportlab.graphics.shapes import Rect, Line, Circle, Polygon, Drawing, Group +from reportlab.graphics.widgets.signsandsymbols import SmileyFace +from reportlab.graphics.widgetbase import Widget +from reportlab.lib.validators import isNumber, isColorOrNone, OneOf, Validator +from reportlab.lib.attrmap import AttrMap, AttrMapValue +from reportlab.lib.colors import black +from reportlab.graphics.widgets.flags import Flag +from math import sin, cos, pi +import copy, new +_toradians = pi/180.0 + +class Marker(Widget): + '''A polymorphic class of markers''' + _attrMap = AttrMap(BASE=Widget, + kind = AttrMapValue( + OneOf(None, 'Square', 'Diamond', 'Circle', 'Cross', 'Triangle', 'StarSix', + 'Pentagon', 'Hexagon', 'Heptagon', 'Octagon', 'StarFive', + 'FilledSquare', 'FilledCircle', 'FilledDiamond', 'FilledCross', + 'FilledTriangle','FilledStarSix', 'FilledPentagon', 'FilledHexagon', + 'FilledHeptagon', 'FilledOctagon', 'FilledStarFive', + 'Smiley'), + desc='marker type name'), + size = AttrMapValue(isNumber,desc='marker size'), + x = AttrMapValue(isNumber,desc='marker x coordinate'), + y = AttrMapValue(isNumber,desc='marker y coordinate'), + dx = AttrMapValue(isNumber,desc='marker x coordinate adjustment'), + dy = AttrMapValue(isNumber,desc='marker y coordinate adjustment'), + angle = AttrMapValue(isNumber,desc='marker rotation'), + fillColor = AttrMapValue(isColorOrNone, desc='marker fill colour'), + strokeColor = AttrMapValue(isColorOrNone, desc='marker stroke colour'), + strokeWidth = AttrMapValue(isNumber, desc='marker stroke width'), + ) + + def __init__(self,*args,**kw): + self.kind = None + self.strokeColor = black + self.strokeWidth = 0.1 + self.fillColor = None + self.size = 5 + self.x = self.y = self.dx = self.dy = self.angle = 0 + self.setProperties(kw) + + def clone(self): + return new.instance(self.__class__,self.__dict__.copy()) + + def _Smiley(self): + x, y = self.x+self.dx, self.y+self.dy + d = self.size/2.0 + s = SmileyFace() + s.fillColor = self.fillColor + s.strokeWidth = self.strokeWidth + s.strokeColor = self.strokeColor + s.x = x-d + s.y = y-d + s.size = d*2 + return s + + def _Square(self): + x, y = self.x+self.dx, self.y+self.dy + d = self.size/2.0 + s = Rect(x-d,y-d,2*d,2*d,fillColor=self.fillColor,strokeColor=self.strokeColor,strokeWidth=self.strokeWidth) + return s + + def _Diamond(self): + d = self.size/2.0 + return self._doPolygon((-d,0,0,d,d,0,0,-d)) + + def _Circle(self): + x, y = self.x+self.dx, self.y+self.dy + s = Circle(x,y,self.size/2.0,fillColor=self.fillColor,strokeColor=self.strokeColor,strokeWidth=self.strokeWidth) + return s + + def _Cross(self): + x, y = self.x+self.dx, self.y+self.dy + s = float(self.size) + h, s = s/2, s/6 + return self._doPolygon((-s,-h,-s,-s,-h,-s,-h,s,-s,s,-s,h,s,h,s,s,h,s,h,-s,s,-s,s,-h)) + + def _Triangle(self): + x, y = self.x+self.dx, self.y+self.dy + r = float(self.size)/2 + c = 30*_toradians + s = sin(30*_toradians)*r + c = cos(c)*r + return self._doPolygon((0,r,-c,-s,c,-s)) + + def _StarSix(self): + r = float(self.size)/2 + c = 30*_toradians + s = sin(c)*r + c = cos(c)*r + z = s/2 + g = c/2 + return self._doPolygon((0,r,-z,s,-c,s,-s,0,-c,-s,-z,-s,0,-r,z,-s,c,-s,s,0,c,s,z,s)) + + def _StarFive(self): + R = float(self.size)/2 + r = R*sin(18*_toradians)/cos(36*_toradians) + P = [] + angle = 90 + for i in xrange(5): + for radius in R, r: + theta = angle*_toradians + P.append(radius*cos(theta)) + P.append(radius*sin(theta)) + angle = angle + 36 + return self._doPolygon(P) + + def _Pentagon(self): + return self._doNgon(5) + + def _Hexagon(self): + return self._doNgon(6) + + def _Heptagon(self): + return self._doNgon(7) + + def _Octagon(self): + return self._doNgon(8) + + def _doPolygon(self,P): + x, y = self.x+self.dx, self.y+self.dy + if x or y: P = map(lambda i,P=P,A=[x,y]: P[i] + A[i&1], range(len(P))) + return Polygon(P, strokeWidth =self.strokeWidth, strokeColor=self.strokeColor, fillColor=self.fillColor) + + def _doFill(self): + old = self.fillColor + if old is None: + self.fillColor = self.strokeColor + r = (self.kind and getattr(self,'_'+self.kind[6:]) or Group)() + self.fillColor = old + return r + + def _doNgon(self,n): + P = [] + size = float(self.size)/2 + for i in xrange(n): + r = (2.*i/n+0.5)*pi + P.append(size*cos(r)) + P.append(size*sin(r)) + return self._doPolygon(P) + + _FilledCircle = _doFill + _FilledSquare = _doFill + _FilledDiamond = _doFill + _FilledCross = _doFill + _FilledTriangle = _doFill + _FilledStarSix = _doFill + _FilledPentagon = _doFill + _FilledHexagon = _doFill + _FilledHeptagon = _doFill + _FilledOctagon = _doFill + _FilledStarFive = _doFill + + def draw(self): + if self.kind: + m = getattr(self,'_'+self.kind) + if self.angle: + _x, _dx, _y, _dy = self.x, self.dx, self.y, self.dy + self.x, self.dx, self.y, self.dy = 0,0,0,0 + try: + m = m() + finally: + self.x, self.dx, self.y, self.dy = _x, _dx, _y, _dy + if not isinstance(m,Group): + _m, m = m, Group() + m.add(_m) + if self.angle: m.rotate(self.angle) + x, y = _x+_dx, _y+_dy + if x or y: m.shift(x,y) + else: + m = m() + else: + m = Group() + return m + +def uSymbol2Symbol(uSymbol,x,y,color): + if type(uSymbol) == FunctionType: + symbol = uSymbol(x, y, 5, color) + elif type(uSymbol) == ClassType and issubclass(uSymbol,Widget): + size = 10. + symbol = uSymbol() + symbol.x = x - (size/2) + symbol.y = y - (size/2) + try: + symbol.size = size + symbol.color = color + except: + pass + elif isinstance(uSymbol,Marker) or isinstance(uSymbol,Flag): + symbol = uSymbol.clone() + if isinstance(uSymbol,Marker): symbol.fillColor = symbol.fillColor or color + symbol.x, symbol.y = x, y + else: + symbol = None + return symbol + +class _isSymbol(Validator): + def test(self,x): + return callable(x) or isinstance(x,Marker) or isinstance(x,Flag) \ + or (type(x)==ClassType and issubclass(x,Widget)) + +isSymbol = _isSymbol() + +def makeMarker(name,**kw): + if Marker._attrMap['kind'].validate(name): + m = apply(Marker,(),kw) + m.kind = name + elif name[-5:]=='_Flag' and Flag._attrMap['kind'].validate(name[:-5]): + m = apply(Flag,(),kw) + m.kind = name[:-5] + m.size = 10 + else: + raise ValueError, "Invalid marker name %s" % name + return m + +if __name__=='__main__': + D = Drawing() + D.add(Marker()) + D.save(fnRoot='Marker',formats=['pdf'], outDir='/tmp') diff --git a/bin/reportlab/graphics/widgets/signsandsymbols.py b/bin/reportlab/graphics/widgets/signsandsymbols.py new file mode 100644 index 00000000000..743b02be748 --- /dev/null +++ b/bin/reportlab/graphics/widgets/signsandsymbols.py @@ -0,0 +1,919 @@ +#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/widgets/signsandsymbols.py +# signsandsymbols.py +# A collection of new widgets +# author: John Precedo (johnp@reportlab.com) +"""This file is a collection of widgets to produce some common signs and symbols. + +Widgets include: +- ETriangle (an equilateral triangle), +- RTriangle (a right angled triangle), +- Octagon, +- Crossbox, +- Tickbox, +- SmileyFace, +- StopSign, +- NoEntry, +- NotAllowed (the red roundel from 'no smoking' signs), +- NoSmoking, +- DangerSign (a black exclamation point in a yellow triangle), +- YesNo (returns a tickbox or a crossbox depending on a testvalue), +- FloppyDisk, +- ArrowOne, and +- ArrowTwo +""" +__version__=''' $Id$ ''' + +from reportlab.lib import colors +from reportlab.lib.validators import * +from reportlab.lib.attrmap import * +from reportlab.graphics import shapes +from reportlab.graphics.widgetbase import Widget +from reportlab.graphics import renderPDF + + +class _Symbol(Widget): + """Abstract base widget + possible attributes: + 'x', 'y', 'size', 'fillColor', 'strokeColor' + """ + _nodoc = 1 + _attrMap = AttrMap( + x = AttrMapValue(isNumber,desc='symbol x coordinate'), + y = AttrMapValue(isNumber,desc='symbol y coordinate'), + dx = AttrMapValue(isNumber,desc='symbol x coordinate adjustment'), + dy = AttrMapValue(isNumber,desc='symbol x coordinate adjustment'), + size = AttrMapValue(isNumber), + fillColor = AttrMapValue(isColorOrNone), + strokeColor = AttrMapValue(isColorOrNone), + strokeWidth = AttrMapValue(isNumber), + ) + def __init__(self): + assert self.__class__.__name__!='_Symbol', 'Abstract class _Symbol instantiated' + self.x = self.y = self.dx = self.dy = 0 + self.size = 100 + self.fillColor = colors.red + self.strokeColor = None + self.strokeWidth = 0.1 + + def demo(self): + D = shapes.Drawing(200, 100) + s = float(self.size) + ob = self.__class__() + ob.x=50 + ob.y=0 + ob.draw() + D.add(ob) + D.add(shapes.String(ob.x+(s/2),(ob.y-12), + ob.__class__.__name__, fillColor=colors.black, textAnchor='middle', + fontSize=10)) + return D + +class ETriangle(_Symbol): + """This draws an equilateral triangle.""" + + def __init__(self): + pass #AbstractSymbol + + def draw(self): + # general widget bits + s = float(self.size) # abbreviate as we will use this a lot + g = shapes.Group() + + # Triangle specific bits + ae = s*0.125 #(ae = 'an eighth') + triangle = shapes.Polygon(points = [ + self.x, self.y, + self.x+s, self.y, + self.x+(s/2),self.y+s], + fillColor = self.fillColor, + strokeColor = self.strokeColor, + strokeWidth=s/50.) + g.add(triangle) + return g + +class RTriangle(_Symbol): + """This draws a right-angled triangle. + + possible attributes: + 'x', 'y', 'size', 'fillColor', 'strokeColor' + + """ + + def __init__(self): + self.x = 0 + self.y = 0 + self.size = 100 + self.fillColor = colors.green + self.strokeColor = None + + def draw(self): + # general widget bits + s = float(self.size) # abbreviate as we will use this a lot + g = shapes.Group() + + # Triangle specific bits + ae = s*0.125 #(ae = 'an eighth') + triangle = shapes.Polygon(points = [ + self.x, self.y, + self.x+s, self.y, + self.x,self.y+s], + fillColor = self.fillColor, + strokeColor = self.strokeColor, + strokeWidth=s/50.) + g.add(triangle) + return g + +class Octagon(_Symbol): + """This widget draws an Octagon. + + possible attributes: + 'x', 'y', 'size', 'fillColor', 'strokeColor' + + """ + + def __init__(self): + self.x = 0 + self.y = 0 + self.size = 100 + self.fillColor = colors.yellow + self.strokeColor = None + + def draw(self): + # general widget bits + s = float(self.size) # abbreviate as we will use this a lot + g = shapes.Group() + + # Octagon specific bits + athird=s/3 + + octagon = shapes.Polygon(points=[self.x+athird, self.y, + self.x, self.y+athird, + self.x, self.y+(athird*2), + self.x+athird, self.y+s, + self.x+(athird*2), self.y+s, + self.x+s, self.y+(athird*2), + self.x+s, self.y+athird, + self.x+(athird*2), self.y], + strokeColor = self.strokeColor, + fillColor = self.fillColor, + strokeWidth=10) + g.add(octagon) + return g + +class Crossbox(_Symbol): + """This draws a black box with a red cross in it - a 'checkbox'. + + possible attributes: + 'x', 'y', 'size', 'crossColor', 'strokeColor', 'crosswidth' + + """ + + _attrMap = AttrMap(BASE=_Symbol, + crossColor = AttrMapValue(isColorOrNone), + crosswidth = AttrMapValue(isNumber), + ) + + def __init__(self): + self.x = 0 + self.y = 0 + self.size = 100 + self.fillColor = colors.white + self.crossColor = colors.red + self.strokeColor = colors.black + self.crosswidth = 10 + + def draw(self): + # general widget bits + s = float(self.size) # abbreviate as we will use this a lot + g = shapes.Group() + + # crossbox specific bits + box = shapes.Rect(self.x+1, self.y+1, s-2, s-2, + fillColor = self.fillColor, + strokeColor = self.strokeColor, + strokeWidth=2) + g.add(box) + + crossLine1 = shapes.Line(self.x+(s*0.15), self.y+(s*0.15), self.x+(s*0.85), self.y+(s*0.85), + fillColor = self.crossColor, + strokeColor = self.crossColor, + strokeWidth = self.crosswidth) + g.add(crossLine1) + + crossLine2 = shapes.Line(self.x+(s*0.15), self.y+(s*0.85), self.x+(s*0.85) ,self.y+(s*0.15), + fillColor = self.crossColor, + strokeColor = self.crossColor, + strokeWidth = self.crosswidth) + g.add(crossLine2) + + return g + + +class Tickbox(_Symbol): + """This draws a black box with a red tick in it - another 'checkbox'. + + possible attributes: + 'x', 'y', 'size', 'tickColor', 'strokeColor', 'tickwidth' + +""" + + _attrMap = AttrMap(BASE=_Symbol, + tickColor = AttrMapValue(isColorOrNone), + tickwidth = AttrMapValue(isNumber), + ) + + def __init__(self): + self.x = 0 + self.y = 0 + self.size = 100 + self.tickColor = colors.red + self.strokeColor = colors.black + self.fillColor = colors.white + self.tickwidth = 10 + + def draw(self): + # general widget bits + s = float(self.size) # abbreviate as we will use this a lot + g = shapes.Group() + + # tickbox specific bits + box = shapes.Rect(self.x+1, self.y+1, s-2, s-2, + fillColor = self.fillColor, + strokeColor = self.strokeColor, + strokeWidth=2) + g.add(box) + + tickLine = shapes.PolyLine(points = [self.x+(s*0.15), self.y+(s*0.35), self.x+(s*0.35), self.y+(s*0.15), + self.x+(s*0.35), self.y+(s*0.15), self.x+(s*0.85) ,self.y+(s*0.85)], + fillColor = self.tickColor, + strokeColor = self.tickColor, + strokeWidth = self.tickwidth) + g.add(tickLine) + + return g + +class SmileyFace(_Symbol): + """This draws a classic smiley face. + + possible attributes: + 'x', 'y', 'size', 'fillColor' + + """ + + def __init__(self): + _Symbol.__init__(self) + self.x = 0 + self.y = 0 + self.size = 100 + self.fillColor = colors.yellow + self.strokeColor = colors.black + + def draw(self): + # general widget bits + s = float(self.size) # abbreviate as we will use this a lot + g = shapes.Group() + + # SmileyFace specific bits + g.add(shapes.Circle(cx=self.x+(s/2), cy=self.y+(s/2), r=s/2, + fillColor=self.fillColor, strokeColor=self.strokeColor, + strokeWidth=max(s/38.,self.strokeWidth))) + + for i in (1,2): + g.add(shapes.Ellipse(self.x+(s/3)*i,self.y+(s/3)*2, s/30, s/10, + fillColor=self.strokeColor, strokeColor = self.strokeColor, + strokeWidth=max(s/38.,self.strokeWidth))) + + # calculate a pointslist for the mouth + # THIS IS A HACK! - don't use if there is a 'shapes.Arc' + centerx=self.x+(s/2) + centery=self.y+(s/2) + radius=s/3 + yradius = radius + xradius = radius + startangledegrees=200 + endangledegrees=340 + degreedelta = 1 + pointslist = [] + a = pointslist.append + from math import sin, cos, pi + degreestoradians = pi/180.0 + radiansdelta = degreedelta*degreestoradians + startangle = startangledegrees*degreestoradians + endangle = endangledegrees*degreestoradians + while endangle<startangle: + endangle = endangle+2*pi + angle = startangle + while angle<endangle: + x = centerx + cos(angle)*radius + y = centery + sin(angle)*yradius + a(x); a(y) + angle = angle+radiansdelta + + # make the mouth + smile = shapes.PolyLine(pointslist, + fillColor = self.strokeColor, + strokeColor = self.strokeColor, + strokeWidth = max(s/38.,self.strokeWidth)) + g.add(smile) + + return g + +class StopSign(_Symbol): + """This draws a (British) stop sign. + + possible attributes: + 'x', 'y', 'size' + + """ + _attrMap = AttrMap(BASE=_Symbol, + stopColor = AttrMapValue(isColorOrNone,desc='color of the word stop'), + ) + + def __init__(self): + self.x = 0 + self.y = 0 + self.size = 100 + self.strokeColor = colors.black + self.fillColor = colors.orangered + self.stopColor = colors.ghostwhite + + def draw(self): + # general widget bits + s = float(self.size) # abbreviate as we will use this a lot + g = shapes.Group() + + # stop-sign specific bits + athird=s/3 + + outerOctagon = shapes.Polygon(points=[self.x+athird, self.y, + self.x, self.y+athird, + self.x, self.y+(athird*2), + self.x+athird, self.y+s, + self.x+(athird*2), self.y+s, + self.x+s, self.y+(athird*2), + self.x+s, self.y+athird, + self.x+(athird*2), self.y], + strokeColor = self.strokeColor, + fillColor = None, + strokeWidth=1) + g.add(outerOctagon) + + innerOctagon = shapes.Polygon(points=[self.x+athird+(s/75), self.y+(s/75), + self.x+(s/75), self.y+athird+(s/75), + self.x+(s/75), self.y+(athird*2)-(s/75), + self.x+athird+(s/75), self.y+s-(s/75), + self.x+(athird*2)-(s/75), (self.y+s)-(s/75), + (self.x+s)-(s/75), self.y+(athird*2)-(s/75), + (self.x+s)-(s/75), self.y+athird+(s/75), + self.x+(athird*2)-(s/75), self.y+(s/75)], + strokeColor = None, + fillColor = self.fillColor, + strokeWidth=0) + g.add(innerOctagon) + + if self.stopColor: + g.add(shapes.String(self.x+(s*0.5),self.y+(s*0.4), + 'STOP', fillColor=self.stopColor, textAnchor='middle', + fontSize=s/3, fontName="Helvetica-Bold")) + + return g + + +class NoEntry(_Symbol): + """This draws a (British) No Entry sign - a red circle with a white line on it. + + possible attributes: + 'x', 'y', 'size' + + """ + + _attrMap = AttrMap(BASE=_Symbol, + innerBarColor = AttrMapValue(isColorOrNone,desc='color of the inner bar'), + ) + + def __init__(self): + self.x = 0 + self.y = 0 + self.size = 100 + self.strokeColor = colors.black + self.fillColor = colors.orangered + self.innerBarColor = colors.ghostwhite + + def draw(self): + # general widget bits + s = float(self.size) # abbreviate as we will use this a lot + g = shapes.Group() + + # no-entry-sign specific bits + if self.strokeColor: + g.add(shapes.Circle(cx = (self.x+(s/2)), cy = (self.y+(s/2)), r = s/2, fillColor = None, strokeColor = self.strokeColor, strokeWidth=1)) + + if self.fillColor: + g.add(shapes.Circle(cx = (self.x+(s/2)), cy =(self.y+(s/2)), r = ((s/2)-(s/50)), fillColor = self.fillColor, strokeColor = None, strokeWidth=0)) + + innerBarColor = self.innerBarColor + if innerBarColor: + g.add(shapes.Rect(self.x+(s*0.1), self.y+(s*0.4), width=s*0.8, height=s*0.2, fillColor = innerBarColor, strokeColor = innerBarColor, strokeLineCap = 1, strokeWidth = 0)) + return g + +class NotAllowed(_Symbol): + """This draws a 'forbidden' roundel (as used in the no-smoking sign). + + possible attributes: + 'x', 'y', 'size' + + """ + + _attrMap = AttrMap(BASE=_Symbol, + ) + + def __init__(self): + self.x = 0 + self.y = 0 + self.size = 100 + self.strokeColor = colors.red + self.fillColor = colors.white + + def draw(self): + # general widget bits + s = float(self.size) # abbreviate as we will use this a lot + g = shapes.Group() + strokeColor = self.strokeColor + + # not=allowed specific bits + outerCircle = shapes.Circle(cx = (self.x+(s/2)), cy = (self.y+(s/2)), r = (s/2)-(s/10), fillColor = self.fillColor, strokeColor = strokeColor, strokeWidth=s/10.) + g.add(outerCircle) + + centerx=self.x+s + centery=self.y+(s/2)-(s/6) + radius=s-(s/6) + yradius = radius/2 + xradius = radius/2 + startangledegrees=100 + endangledegrees=-80 + degreedelta = 90 + pointslist = [] + a = pointslist.append + from math import sin, cos, pi + degreestoradians = pi/180.0 + radiansdelta = degreedelta*degreestoradians + startangle = startangledegrees*degreestoradians + endangle = endangledegrees*degreestoradians + while endangle<startangle: + endangle = endangle+2*pi + angle = startangle + while angle<endangle: + x = centerx + cos(angle)*radius + y = centery + sin(angle)*yradius + a(x); a(y) + angle = angle+radiansdelta + crossbar = shapes.PolyLine(pointslist, fillColor = strokeColor, strokeColor = strokeColor, strokeWidth = s/10.) + g.add(crossbar) + return g + + +class NoSmoking(NotAllowed): + """This draws a no-smoking sign. + + possible attributes: + 'x', 'y', 'size' + + """ + + def __init__(self): + NotAllowed.__init__(self) + + def draw(self): + # general widget bits + s = float(self.size) # abbreviate as we will use this a lot + g = NotAllowed.draw(self) + + # no-smoking-sign specific bits + newx = self.x+(s/2)-(s/3.5) + newy = self.y+(s/2)-(s/32) + cigarrette1 = shapes.Rect(x = newx, y = newy, width = (s/2), height =(s/16), + fillColor = colors.ghostwhite, strokeColor = colors.gray, strokeWidth=0) + newx=newx+(s/2)+(s/64) + g.insert(-1,cigarrette1) + + cigarrette2 = shapes.Rect(x = newx, y = newy, width = (s/80), height =(s/16), + fillColor = colors.orangered, strokeColor = None, strokeWidth=0) + newx= newx+(s/35) + g.insert(-1,cigarrette2) + + cigarrette3 = shapes.Rect(x = newx, y = newy, width = (s/80), height =(s/16), + fillColor = colors.orangered, strokeColor = None, strokeWidth=0) + newx= newx+(s/35) + g.insert(-1,cigarrette3) + + cigarrette4 = shapes.Rect(x = newx, y = newy, width = (s/80), height =(s/16), + fillColor = colors.orangered, strokeColor = None, strokeWidth=0) + newx= newx+(s/35) + g.insert(-1,cigarrette4) + + return g + + +class DangerSign(_Symbol): + """This draws a 'danger' sign: a yellow box with a black exclamation point. + + possible attributes: + 'x', 'y', 'size', 'strokeColor', 'fillColor', 'strokeWidth' + + """ + + def __init__(self): + self.x = 0 + self.y = 0 + self.size = 100 + self.strokeColor = colors.black + self.fillColor = colors.gold + self.strokeWidth = self.size*0.125 + + def draw(self): + # general widget bits + s = float(self.size) # abbreviate as we will use this a lot + g = shapes.Group() + ew = self.strokeWidth + ae = s*0.125 #(ae = 'an eighth') + + + # danger sign specific bits + + ew = self.strokeWidth + ae = s*0.125 #(ae = 'an eighth') + + outerTriangle = shapes.Polygon(points = [ + self.x, self.y, + self.x+s, self.y, + self.x+(s/2),self.y+s], + fillColor = None, + strokeColor = self.strokeColor, + strokeWidth=0) + g.add(outerTriangle) + + innerTriangle = shapes.Polygon(points = [ + self.x+(s/50), self.y+(s/75), + (self.x+s)-(s/50), self.y+(s/75), + self.x+(s/2),(self.y+s)-(s/50)], + fillColor = self.fillColor, + strokeColor = None, + strokeWidth=0) + g.add(innerTriangle) + + exmark = shapes.Polygon(points=[ + ((self.x+s/2)-ew/2), self.y+ae*2.5, + ((self.x+s/2)+ew/2), self.y+ae*2.5, + ((self.x+s/2)+((ew/2))+(ew/6)), self.y+ae*5.5, + ((self.x+s/2)-((ew/2))-(ew/6)), self.y+ae*5.5], + fillColor = self.strokeColor, + strokeColor = None) + g.add(exmark) + + exdot = shapes.Polygon(points=[ + ((self.x+s/2)-ew/2), self.y+ae, + ((self.x+s/2)+ew/2), self.y+ae, + ((self.x+s/2)+ew/2), self.y+ae*2, + ((self.x+s/2)-ew/2), self.y+ae*2], + fillColor = self.strokeColor, + strokeColor = None) + g.add(exdot) + + return g + + +class YesNo(_Symbol): + """This widget draw a tickbox or crossbox depending on 'testValue'. + + If this widget is supplied with a 'True' or 1 as a value for + testValue, it will use the tickbox widget. Otherwise, it will + produce a crossbox. + + possible attributes: + 'x', 'y', 'size', 'tickcolor', 'crosscolor', 'testValue' + +""" + + _attrMap = AttrMap(BASE=_Symbol, + tickcolor = AttrMapValue(isColor), + crosscolor = AttrMapValue(isColor), + testValue = AttrMapValue(isBoolean), + ) + + def __init__(self): + self.x = 0 + self.y = 0 + self.size = 100 + self.tickcolor = colors.green + self.crosscolor = colors.red + self.testValue = 1 + + def draw(self): + if self.testValue: + yn=Tickbox() + yn.tickColor=self.tickcolor + else: + yn=Crossbox() + yn.crossColor=self.crosscolor + yn.x=self.x + yn.y=self.y + yn.size=self.size + yn.draw() + return yn + + + def demo(self): + D = shapes.Drawing(200, 100) + yn = YesNo() + yn.x = 15 + yn.y = 25 + yn.size = 70 + yn.testValue = 0 + yn.draw() + D.add(yn) + yn2 = YesNo() + yn2.x = 120 + yn2.y = 25 + yn2.size = 70 + yn2.testValue = 1 + yn2.draw() + D.add(yn2) + labelFontSize = 8 + D.add(shapes.String(yn.x+(yn.size/2),(yn.y-(1.2*labelFontSize)), + 'testValue=0', fillColor=colors.black, textAnchor='middle', + fontSize=labelFontSize)) + D.add(shapes.String(yn2.x+(yn2.size/2),(yn2.y-(1.2*labelFontSize)), + 'testValue=1', fillColor=colors.black, textAnchor='middle', + fontSize=labelFontSize)) + labelFontSize = 10 + D.add(shapes.String(yn.x+85,(yn.y-20), + self.__class__.__name__, fillColor=colors.black, textAnchor='middle', + fontSize=labelFontSize)) + return D + +class FloppyDisk(_Symbol): + """This widget draws an icon of a floppy disk. + + possible attributes: + 'x', 'y', 'size', 'diskcolor' + + """ + + _attrMap = AttrMap(BASE=_Symbol, + diskColor = AttrMapValue(isColor), + ) + + def __init__(self): + self.x = 0 + self.y = 0 + self.size = 100 + self.diskColor = colors.black + + def draw(self): + # general widget bits + s = float(self.size) # abbreviate as we will use this a lot + g = shapes.Group() + + + # floppy disk specific bits + diskBody = shapes.Rect(x=self.x, y=self.y+(s/100), width=s, height=s-(s/100), + fillColor = self.diskColor, + strokeColor = None, + strokeWidth=0) + g.add(diskBody) + + label = shapes.Rect(x=self.x+(s*0.1), y=(self.y+s)-(s*0.5), width=s*0.8, height=s*0.48, + fillColor = colors.whitesmoke, + strokeColor = None, + strokeWidth=0) + g.add(label) + + labelsplash = shapes.Rect(x=self.x+(s*0.1), y=(self.y+s)-(s*0.1), width=s*0.8, height=s*0.08, + fillColor = colors.royalblue, + strokeColor = None, + strokeWidth=0) + g.add(labelsplash) + + + line1 = shapes.Line(x1=self.x+(s*0.15), y1=self.y+(0.6*s), x2=self.x+(s*0.85), y2=self.y+(0.6*s), + fillColor = colors.black, + strokeColor = colors.black, + strokeWidth=0) + g.add(line1) + + line2 = shapes.Line(x1=self.x+(s*0.15), y1=self.y+(0.7*s), x2=self.x+(s*0.85), y2=self.y+(0.7*s), + fillColor = colors.black, + strokeColor = colors.black, + strokeWidth=0) + g.add(line2) + + line3 = shapes.Line(x1=self.x+(s*0.15), y1=self.y+(0.8*s), x2=self.x+(s*0.85), y2=self.y+(0.8*s), + fillColor = colors.black, + strokeColor = colors.black, + strokeWidth=0) + g.add(line3) + + metalcover = shapes.Rect(x=self.x+(s*0.2), y=(self.y), width=s*0.5, height=s*0.35, + fillColor = colors.silver, + strokeColor = None, + strokeWidth=0) + g.add(metalcover) + + coverslot = shapes.Rect(x=self.x+(s*0.28), y=(self.y)+(s*0.035), width=s*0.12, height=s*0.28, + fillColor = self.diskColor, + strokeColor = None, + strokeWidth=0) + g.add(coverslot) + + return g + +class ArrowOne(_Symbol): + """This widget draws an arrow (style one). + + possible attributes: + 'x', 'y', 'size', 'fillColor' + + """ + def __init__(self): + self.x = 0 + self.y = 0 + self.size = 100 + self.fillColor = colors.red + + def draw(self): + # general widget bits + s = float(self.size) # abbreviate as we will use this a lot + g = shapes.Group() + + + # arrow specific bits + body = shapes.Rect(x=self.x, y=(self.y+(s/2))-(s/6), width=2*(s/3), height=(s/3), + fillColor = self.fillColor, + strokeColor = None, + strokeWidth=0) + g.add(body) + + head = shapes.Polygon(points = [self.x+(3*(s/6)), (self.y+(s/2)), + self.x+(3*(s/6)), self.y+8*(s/10), + self.x+s, self.y+(s/2), + self.x+(3*(s/6)), self.y+2*(s/10)], + fillColor = self.fillColor, + strokeColor = None, + strokeWidth=0) + g.add(head) + + return g + +class ArrowTwo(ArrowOne): + """This widget draws an arrow (style two). + + possible attributes: + 'x', 'y', 'size', 'fillColor' + + """ + + def __init__(self): + self.x = 0 + self.y = 0 + self.size = 100 + self.fillColor = colors.blue + + def draw(self): + # general widget bits + s = float(self.size) # abbreviate as we will use this a lot + g = shapes.Group() + + + # arrow specific bits + body = shapes.Rect(x=self.x, y=(self.y+(s/2))-(s/24), width=9*(s/10), height=(s/12), + fillColor = self.fillColor, + strokeColor = None, + strokeWidth=0) + g.add(body) + + head = shapes.Polygon(points = [self.x+(2.5*(s/3)), (self.y+(s/2)), + self.x+(4*(s/6)), self.y+4*(s/6), + self.x+s, self.y+(s/2), + self.x+(4*(s/6)), self.y+2*(s/6)], + fillColor = self.fillColor, + strokeColor = None, + strokeWidth=0) + g.add(head) + + return g + + +def test(): + """This function produces a pdf with examples of all the signs and symbols from this file. + """ + labelFontSize = 10 + D = shapes.Drawing(450,650) + cb = Crossbox() + cb.x = 20 + cb.y = 530 + D.add(cb) + D.add(shapes.String(cb.x+(cb.size/2),(cb.y-(1.2*labelFontSize)), + cb.__class__.__name__, fillColor=colors.black, textAnchor='middle', + fontSize=labelFontSize)) + + tb = Tickbox() + tb.x = 170 + tb.y = 530 + D.add(tb) + D.add(shapes.String(tb.x+(tb.size/2),(tb.y-(1.2*labelFontSize)), + tb.__class__.__name__, fillColor=colors.black, textAnchor='middle', + fontSize=labelFontSize)) + + + yn = YesNo() + yn.x = 320 + yn.y = 530 + D.add(yn) + tempstring = yn.__class__.__name__ + '*' + D.add(shapes.String(yn.x+(tb.size/2),(yn.y-(1.2*labelFontSize)), + tempstring, fillColor=colors.black, textAnchor='middle', + fontSize=labelFontSize)) + D.add(shapes.String(130,6, + "(The 'YesNo' widget returns a tickbox if testvalue=1, and a crossbox if testvalue=0)", fillColor=colors.black, textAnchor='middle', + fontSize=labelFontSize*0.75)) + + + ss = StopSign() + ss.x = 20 + ss.y = 400 + D.add(ss) + D.add(shapes.String(ss.x+(ss.size/2), ss.y-(1.2*labelFontSize), + ss.__class__.__name__, fillColor=colors.black, textAnchor='middle', + fontSize=labelFontSize)) + + ne = NoEntry() + ne.x = 170 + ne.y = 400 + D.add(ne) + D.add(shapes.String(ne.x+(ne.size/2),(ne.y-(1.2*labelFontSize)), + ne.__class__.__name__, fillColor=colors.black, textAnchor='middle', + fontSize=labelFontSize)) + + sf = SmileyFace() + sf.x = 320 + sf.y = 400 + D.add(sf) + D.add(shapes.String(sf.x+(sf.size/2),(sf.y-(1.2*labelFontSize)), + sf.__class__.__name__, fillColor=colors.black, textAnchor='middle', + fontSize=labelFontSize)) + + ds = DangerSign() + ds.x = 20 + ds.y = 270 + D.add(ds) + D.add(shapes.String(ds.x+(ds.size/2),(ds.y-(1.2*labelFontSize)), + ds.__class__.__name__, fillColor=colors.black, textAnchor='middle', + fontSize=labelFontSize)) + + na = NotAllowed() + na.x = 170 + na.y = 270 + D.add(na) + D.add(shapes.String(na.x+(na.size/2),(na.y-(1.2*labelFontSize)), + na.__class__.__name__, fillColor=colors.black, textAnchor='middle', + fontSize=labelFontSize)) + + ns = NoSmoking() + ns.x = 320 + ns.y = 270 + D.add(ns) + D.add(shapes.String(ns.x+(ns.size/2),(ns.y-(1.2*labelFontSize)), + ns.__class__.__name__, fillColor=colors.black, textAnchor='middle', + fontSize=labelFontSize)) + + a1 = ArrowOne() + a1.x = 20 + a1.y = 140 + D.add(a1) + D.add(shapes.String(a1.x+(a1.size/2),(a1.y-(1.2*labelFontSize)), + a1.__class__.__name__, fillColor=colors.black, textAnchor='middle', + fontSize=labelFontSize)) + + a2 = ArrowTwo() + a2.x = 170 + a2.y = 140 + D.add(a2) + D.add(shapes.String(a2.x+(a2.size/2),(a2.y-(1.2*labelFontSize)), + a2.__class__.__name__, fillColor=colors.black, textAnchor='middle', + fontSize=labelFontSize)) + + fd = FloppyDisk() + fd.x = 320 + fd.y = 140 + D.add(fd) + D.add(shapes.String(fd.x+(fd.size/2),(fd.y-(1.2*labelFontSize)), + fd.__class__.__name__, fillColor=colors.black, textAnchor='middle', + fontSize=labelFontSize)) + + renderPDF.drawToFile(D, 'signsandsymbols.pdf', 'signsandsymbols.py') + print 'wrote file: signsandsymbols.pdf' + +if __name__=='__main__': + test()
\ No newline at end of file |
