#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 2844 2006-05-03 12:20:25Z 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
