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