summaryrefslogtreecommitdiff
path: root/bin/reportlab/graphics
diff options
context:
space:
mode:
authorpinky <>2006-12-07 13:41:40 +0000
committerpinky <>2006-12-07 13:41:40 +0000
commit004a0b996ff8f269451e07346f71a129a1f3fbaf (patch)
tree48f43c1fc2ca4c1f4d5648e7e73553d3f4ccae06 /bin/reportlab/graphics
New trunk
bzr revid: pinky-3f10ee12cea3c4c75cef44ab04ad33ef47432907
Diffstat (limited to 'bin/reportlab/graphics')
-rw-r--r--bin/reportlab/graphics/__init__.py4
-rw-r--r--bin/reportlab/graphics/charts/__init__.py4
-rw-r--r--bin/reportlab/graphics/charts/areas.py92
-rw-r--r--bin/reportlab/graphics/charts/axes.py1958
-rw-r--r--bin/reportlab/graphics/charts/barcharts.py1943
-rw-r--r--bin/reportlab/graphics/charts/dotbox.py165
-rw-r--r--bin/reportlab/graphics/charts/doughnut.py372
-rw-r--r--bin/reportlab/graphics/charts/legends.py486
-rw-r--r--bin/reportlab/graphics/charts/linecharts.py656
-rw-r--r--bin/reportlab/graphics/charts/lineplots.py1096
-rw-r--r--bin/reportlab/graphics/charts/markers.py81
-rw-r--r--bin/reportlab/graphics/charts/piecharts.py863
-rw-r--r--bin/reportlab/graphics/charts/slidebox.py186
-rw-r--r--bin/reportlab/graphics/charts/spider.py353
-rw-r--r--bin/reportlab/graphics/charts/textlabels.py440
-rw-r--r--bin/reportlab/graphics/charts/utils.py191
-rw-r--r--bin/reportlab/graphics/charts/utils3d.py233
-rw-r--r--bin/reportlab/graphics/renderPDF.py361
-rw-r--r--bin/reportlab/graphics/renderPM.py631
-rw-r--r--bin/reportlab/graphics/renderPS.py859
-rw-r--r--bin/reportlab/graphics/renderSVG.py813
-rw-r--r--bin/reportlab/graphics/renderbase.py315
-rw-r--r--bin/reportlab/graphics/samples/__init__.py0
-rw-r--r--bin/reportlab/graphics/samples/bubble.py73
-rw-r--r--bin/reportlab/graphics/samples/clustered_bar.py84
-rw-r--r--bin/reportlab/graphics/samples/clustered_column.py83
-rw-r--r--bin/reportlab/graphics/samples/excelcolors.py45
-rw-r--r--bin/reportlab/graphics/samples/exploded_pie.py65
-rw-r--r--bin/reportlab/graphics/samples/filled_radar.py54
-rw-r--r--bin/reportlab/graphics/samples/line_chart.py83
-rw-r--r--bin/reportlab/graphics/samples/linechart_with_markers.py94
-rw-r--r--bin/reportlab/graphics/samples/radar.py66
-rw-r--r--bin/reportlab/graphics/samples/runall.py59
-rw-r--r--bin/reportlab/graphics/samples/scatter.py71
-rw-r--r--bin/reportlab/graphics/samples/scatter_lines.py82
-rw-r--r--bin/reportlab/graphics/samples/scatter_lines_markers.py72
-rw-r--r--bin/reportlab/graphics/samples/simple_pie.py61
-rw-r--r--bin/reportlab/graphics/samples/stacked_bar.py85
-rw-r--r--bin/reportlab/graphics/samples/stacked_column.py84
-rw-r--r--bin/reportlab/graphics/shapes.py1244
-rwxr-xr-xbin/reportlab/graphics/testdrawings.py294
-rwxr-xr-xbin/reportlab/graphics/testshapes.py547
-rw-r--r--bin/reportlab/graphics/widgetbase.py490
-rw-r--r--bin/reportlab/graphics/widgets/__init__.py4
-rw-r--r--bin/reportlab/graphics/widgets/eventcal.py303
-rw-r--r--bin/reportlab/graphics/widgets/flags.py879
-rw-r--r--bin/reportlab/graphics/widgets/grids.py504
-rw-r--r--bin/reportlab/graphics/widgets/markers.py228
-rw-r--r--bin/reportlab/graphics/widgets/signsandsymbols.py919
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 &amp; 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