#Copyright ReportLab Europe Ltd. 2000-2017
#see license.txt for license details
__version__='3.3.0'
__doc__="""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.
"""

from math import log10 as math_log10
from functools import lru_cache
from reportlab.lib.validators import    isNumber, isNumberOrNone, isListOfStringsOrNone, isListOfNumbers, \
                                        isListOfNumbersOrNone, isColorOrNone, OneOf, isBoolean, SequenceOf, \
                                        isString, EitherOr, Validator, NoneOr, \
                                        isNormalDate, isNoneOrCallable
from reportlab.lib.attrmap import *
from reportlab.lib import normalDate
from reportlab.graphics.shapes import Drawing, Line, PolyLine, Rect, Group, STATE_DEFAULTS, _textBoxLimits, _rotatedBoxLimits
from reportlab.graphics.widgetbase import Widget, TypedPropertyCollection
from reportlab.graphics.charts.textlabels import Label, PMVLabel, XLabel,  DirectDrawFlowable
from reportlab.graphics.charts.utils import nextRoundNumber
from reportlab.graphics.widgets.grids import ShadedRect
from reportlab.lib.colors import Color
from reportlab.lib.utils import isSeq

# Helpers.
def _findMinMaxValue(V, x, default, func, special=None, extraMinMaxValues=None):
    if isSeq(V[0][0]):
        if special:
            f=lambda T,x=x,special=special,func=func: special(T,x,func)
        else:
            f=lambda T,x=x: T[x]
        V=list(map(lambda e,f=f: list(map(f,e)),V))
    V = list(filter(len,[[x for x in x if x is not None] for x in V]))
    if len(V)==0: return default
    r = func(list(map(func,V)))
    return func(func(extraMinMaxValues),r) if extraMinMaxValues else r

def _findMin(V, x, default,special=None,extraMinMaxValues=None):
    '''find minimum over V[i][x]'''
    return _findMinMaxValue(V,x,default,min,special=special,extraMinMaxValues=extraMinMaxValues)

def _findMax(V, x, default,special=None,extraMinMaxValues=None):
    '''find maximum over V[i][x]'''
    return _findMinMaxValue(V,x,default,max,special=special, extraMinMaxValues=extraMinMaxValues)

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 AxisLabelAnnotation:
    '''Create a grid like line using the given user value to draw the line
    v       value to use
    kwds may contain
    scaleValue  True/not given --> scale the value
                otherwise use the absolute value
    labelClass  the label class to use default Label
    all Label keywords are acceptable (including say _text)
    '''
    def __init__(self,v,**kwds):
        self._v = v
        self._kwds = kwds

    def __call__(self,axis):
        kwds = self._kwds.copy()
        labelClass = kwds.pop('labelClass',Label)
        scaleValue = kwds.pop('scaleValue',True)
        if not hasattr(axis,'_tickValues'):
            axis._pseudo_configure()
        sv = (axis.scale if scaleValue else lambda x: x)(self._v)
        if axis.isYAxis:
            x = axis._x
            y = sv
        else:
            x = sv
            y = axis._y
        kwds['x'] = x
        kwds['y'] = y
        return labelClass(**kwds)

class AxisLineAnnotation:
    '''Create a grid like line using the given user value to draw the line
    kwds may contain
    startOffset if true v is offset from the default grid start position
    endOffset   if true v is offset from the default grid end position
    scaleValue  True/not given --> scale the value
                otherwise use the absolute value
    lo          lowest coordinate to draw default 0
    hi          highest coordinate to draw at default = length
    drawAtLimit True draw line at appropriate limit if its coordinate exceeds the lo, hi range
                False ignore if it's outside the range
    all Line keywords are acceptable
    '''
    def __init__(self,v,**kwds):
        self._v = v
        self._kwds = kwds

    def __call__(self,axis):
        kwds = self._kwds.copy()
        scaleValue = kwds.pop('scaleValue',True)
        endOffset = kwds.pop('endOffset',False)
        startOffset = kwds.pop('startOffset',False)
        if axis.isYAxis:
            offs = axis._x
            d0 = axis._y
        else:
            offs = axis._y
            d0 = axis._x
        s = kwds.pop('start',None)
        e = kwds.pop('end',None)
        if s is None or e is None:
            dim = getattr(getattr(axis,'joinAxis',None),'getGridDims',None)
            if dim and hasattr(dim,'__call__'):
                dim = dim()
            if dim:
                if s is None: s = dim[0]
                if e is None: e = dim[1]
            else:
                if s is None: s = 0
                if e is None: e = 0
        hi = kwds.pop('hi',axis._length)+d0
        lo = kwds.pop('lo',0)+d0
        lo,hi=min(lo,hi),max(lo,hi)
        drawAtLimit = kwds.pop('drawAtLimit',False)
        oaglp = axis._get_line_pos
        if not scaleValue:
            axis._get_line_pos = lambda x: x
        try:
            v = self._v
            if endOffset:
                v = v + hi
            elif startOffset:
                v = v + lo
            func = axis._getLineFunc(s-offs,e-offs,kwds.pop('parent',None))
            if not hasattr(axis,'_tickValues'):
                axis._pseudo_configure()
            d = axis._get_line_pos(v)
            if d<lo or d>hi:
                if not drawAtLimit: return None
                if d<lo:
                    d = lo
                else:
                    d = hi
                axis._get_line_pos = lambda x: d
            L = func(v)
            for k,v in kwds.items():
                setattr(L,k,v)
            if getattr(self,'_dbg',0):
                print(f'v={v} --> d={d} L={L}')
        finally:
            axis._get_line_pos = oaglp
        return L

class AxisBackgroundAnnotation:
    '''Create a set of coloured bars on the background of a chart using axis ticks as the bar borders
    colors is a set of colors to use for the background bars. A colour of None is just a skip.
    Special effects if you pass a rect or Shaded rect instead.
    '''
    def __init__(self,colors,**kwds):
        self._colors = colors
        self._kwds = kwds

    def __call__(self,axis):
        colors = self._colors
        if not colors: return
        kwds = self._kwds.copy()
        isYAxis = axis.isYAxis
        if isYAxis:
            offs = axis._x
            d0 = axis._y
        else:
            offs = axis._y
            d0 = axis._x
        s = kwds.pop('start',None)
        e = kwds.pop('end',None)
        if s is None or e is None:
            dim = getattr(getattr(axis,'joinAxis',None),'getGridDims',None)
            if dim and hasattr(dim,'__call__'):
                dim = dim()
            if dim:
                if s is None: s = dim[0]
                if e is None: e = dim[1]
            else:
                if s is None: s = 0
                if e is None: e = 0
        if not hasattr(axis,'_tickValues'):
            axis._pseudo_configure()
        tv = getattr(axis,'_tickValues',None)
        if not tv: return
        G = Group()
        ncolors = len(colors)
        v0 = axis._get_line_pos(tv[0])
        for i in range(1,len(tv)):
            v1 = axis._get_line_pos(tv[i])
            c = colors[(i-1)%ncolors]
            if c:
                if isYAxis:
                    y = v0
                    x = s
                    height = v1-v0
                    width = e-s
                else:
                    x = v0
                    y = s
                    width = v1-v0
                    height = e-s
                if isinstance(c,Color):
                    r = Rect(x,y,width,height,fillColor=c,strokeColor=None)
                elif isinstance(c,Rect):
                    r = Rect(x,y,width,height)
                    for k in c.__dict__:
                        if k not in ('x','y','width','height'):
                            setattr(r,k,getattr(c,k))
                elif isinstance(c,ShadedRect):
                    r = ShadedRect(x=x,y=y,width=width,height=height)
                    for k in c.__dict__:
                        if k not in ('x','y','width','height'):
                            setattr(r,k,getattr(c,k))
                G.add(r)
            v0 = v1
        return G

class TickLU:
    '''lookup special cases for tick values'''
    def __init__(self,*T,**kwds):
        self.accuracy = kwds.pop('accuracy',1e-8)
        self.T = T
    def __contains__(self,t):
        accuracy = self.accuracy
        for x,v in self.T:
            if abs(x-t)<accuracy:
                return True
        return False
    def __getitem__(self,t):
        accuracy = self.accuracy
        for x,v in self.T:
            if abs(x-t)<self.accuracy:
                return v
        raise IndexError('cannot locate index %r' % t)

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.isYAxis 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.isYAxis 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,strokeLineJoin,strokeLineCap,strokeMiterLimit,parent=None,exclude=[],specials={}):
        func = self._getLineFunc(start,end,parent)
        if not hasattr(self,'_tickValues'):
            self._pseudo_configure()
        if exclude:
            exf = self.isYAxis and (lambda l: l.y1 in exclude) or (lambda l: l.x1 in exclude)
        else:
            exf = None
        for t in self._tickValues:
                L = func(t)
                if exf and exf(L): continue
                L.strokeColor = strokeColor
                L.strokeWidth = strokeWidth
                L.strokeDashArray = strokeDashArray
                L.strokeLineJoin = strokeLineJoin
                L.strokeLineCap = strokeLineCap
                L.strokeMiterLimit = strokeMiterLimit
                if t in specials:
                    for a,v in specials[t].items():
                        setattr(L,a,v)
                g.add(L)

    def makeGrid(self,g,dim=None,parent=None,exclude=[]):
        '''this is only called by a container object'''
        c = self.gridStrokeColor
        w = self.gridStrokeWidth or 0
        if w and c and self.visibleGrid:
            s = self.gridStart
            e = self.gridEnd
            if s is None or e is None:
                if dim and hasattr(dim,'__call__'):
                    dim = dim()
                if dim:
                    if s is None: s = dim[0]
                    if e is None: e = dim[1]
                else:
                    if s is None: s = 0
                    if e is None: e = 0
            if s or e:
                if self.isYAxis: offs = self._x
                else: offs = self._y
                self._makeLines(g,s-offs,e-offs,c,w,self.gridStrokeDashArray,self.gridStrokeLineJoin,self.gridStrokeLineCap,self.gridStrokeMiterLimit,parent=parent,exclude=exclude,specials=getattr(self,'_gridSpecials',{}))
        self._makeSubGrid(g,dim,parent,exclude=[])

    def _makeSubGrid(self,g,dim=None,parent=None,exclude=[]):
        '''this is only called by a container object'''
        if not (getattr(self,'visibleSubGrid',0) and self.subTickNum>0): return
        c = self.subGridStrokeColor
        w = self.subGridStrokeWidth or 0
        if not(w and c): return
        s = self.subGridStart
        e = self.subGridEnd
        if s is None or e is None:
            if dim and hasattr(dim,'__call__'):
                dim = dim()
            if dim:
                if s is None: s = dim[0]
                if e is None: e = dim[1]
            else:
                if s is None: s = 0
                if e is None: e = 0
        if s or e:
            if self.isYAxis: offs = self._x
            else: offs = self._y
            otv = self._calcSubTicks()
            try:
                self._makeLines(g,s-offs,e-offs,c,w,self.subGridStrokeDashArray,self.subGridStrokeLineJoin,self.subGridStrokeLineCap,self.subGridStrokeMiterLimit,parent=parent,exclude=exclude)
            finally:
                self._tickValues = otv

    def getGridDims(self,start=None,end=None):
        if start is None: start = (self._x,self._y)[self.isYAxis]
        if end is None: end = start+self._length
        return start,end

    def isYAxis(self):
        if getattr(self,'_dataIndex',None)==1: return True
        acn = self.__class__.__name__
        return acn[0]=='Y' or acn[:4]=='AdjY'
    isYAxis = property(isYAxis)

    def isXAxis(self):
        if getattr(self,'_dataIndex',None)==0: return True
        acn = self.__class__.__name__
        return acn[0]=='X' or acn[:11]=='NormalDateX'
    isXAxis = property(isXAxis)

    def addAnnotations(self,g,A=None):
        if A is None: getattr(self,'annotations',[])
        for x in A:
            g.add(x(self))

    def _splitAnnotations(self):
        A = getattr(self,'annotations',[])[:]
        D = {}
        for v in ('early','beforeAxis','afterAxis','beforeTicks',
                'afterTicks','beforeTickLabels',
                'afterTickLabels','late'):
            R = [].append
            P = [].append
            for a in A:
                if getattr(a,v,0):
                    R(a)
                else:
                    P(a)
            D[v] = R.__self__
            A[:] = P.__self__
        D['late'] += A
        return D

    def draw(self):
        g = Group()
        A = self._splitAnnotations()
        self.addAnnotations(g,A['early'])

        if self.visible:
            self.addAnnotations(g,A['beforeAxis'])
            g.add(self.makeAxis())
            self.addAnnotations(g,A['afterAxis'])
            self.addAnnotations(g,A['beforeTicks'])
            g.add(self.makeTicks())
            self.addAnnotations(g,A['afterTicks'])
            self.addAnnotations(g,A['beforeTickLabels'])
            g.add(self.makeTickLabels())
            self.addAnnotations(g,A['afterTickLabels'])

        self.addAnnotations(g,A['late'])
        return g

class CALabel(PMVLabel):
    _attrMap = AttrMap(BASE=PMVLabel,
        labelPosFrac = AttrMapValue(isNumber, desc='where in the category range [0,1] the labels should be anchored'),
        )
    def __init__(self,**kw):
        PMVLabel.__init__(self,**kw)
        self._setKeywords(
                labelPosFrac = 0.5,
                )

# 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.'),
        strokeLineCap = AttrMapValue(OneOf(0,1,2),desc="Line cap 0=butt, 1=round & 2=square"),
        strokeLineJoin = AttrMapValue(OneOf(0,1,2),desc="Line join 0=miter, 1=round & 2=bevel"),
        strokeMiterLimit = AttrMapValue(isNumber,desc="miter limit control miter line joins"),
        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.'),
        gridStrokeLineCap = AttrMapValue(OneOf(0,1,2),desc="Grid Line cap 0=butt, 1=round & 2=square"),
        gridStrokeLineJoin = AttrMapValue(OneOf(0,1,2),desc="Grid Line join 0=miter, 1=round & 2=bevel"),
        gridStrokeMiterLimit = AttrMapValue(isNumber,desc="Grid miter limit control miter line joins"),
        gridStart = AttrMapValue(isNumberOrNone, desc='Start of grid lines wrt axis origin'),
        gridEnd = AttrMapValue(isNumberOrNone, desc='End of grid lines wrt axis origin'),
        drawGridLast = AttrMapValue(isBoolean, desc='if true draw gridlines after everything else.'),
        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', 'axispmv'), desc="Like joinAxisMode, but for the axis labels"),
        tickShift = AttrMapValue(isBoolean, desc='Tick shift typically'),
        tickStrokeWidth = AttrMapValue(isNumberOrNone, desc='Width of ticks if specified.'),
        tickStrokeColor = AttrMapValue(isColorOrNone, desc='Color of ticks if specified.'),
        loPad = AttrMapValue(isNumber, desc='extra inner space before start of the axis'),
        hiPad = AttrMapValue(isNumber, desc='extra inner space after end of the axis'),
        annotations = AttrMapValue(None,desc='list of annotations'),
        loLLen = AttrMapValue(isNumber, desc='extra line length before start of the axis'),
        hiLLen = AttrMapValue(isNumber, desc='extra line length after end of the axis'),
        skipGrid = AttrMapValue(OneOf('none','top','both','bottom'),"grid lines to skip top bottom both none"),
        innerTickDraw = AttrMapValue(isNoneOrCallable, desc="Callable to replace _drawInnerTicks"),
        )

    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.drawGridLast = False

        self.strokeWidth = 1
        self.strokeColor = STATE_DEFAULTS['strokeColor']
        self.strokeDashArray = STATE_DEFAULTS['strokeDashArray']

        self.gridStrokeLineJoin = self.strokeLineJoin = STATE_DEFAULTS['strokeLineJoin']
        self.gridStrokeLineCap = self.strokeLineCap = STATE_DEFAULTS['strokeLineCap']
        self.gridStrokeMiterLimit = self.strokeMiterLimit = STATE_DEFAULTS['strokeMiterLimit']
        self.gridStrokeWidth = 0.25
        self.gridStrokeColor = STATE_DEFAULTS['strokeColor']
        self.gridStrokeDashArray = STATE_DEFAULTS['strokeDashArray']
        self.gridStart = self.gridEnd = None

        self.strokeLineJoin = STATE_DEFAULTS['strokeLineJoin']
        self.strokeLineCap = STATE_DEFAULTS['strokeLineCap']
        self.strokeMiterLimit = STATE_DEFAULTS['strokeMiterLimit']

        self.labels = TypedPropertyCollection(CALabel)
        # 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
        self.loPad = 0
        self.hiPad = 0
        self.loLLen = 0
        self.hiLLen = 0

    def setPosition(self, x, y, length):
        # ensure floating point
        self._x = float(x)
        self._y = float(y)
        self._length = float(length)

    def configure(self, multiSeries,barWidth=None):
        self._catCount = max(list(map(len,multiSeries)))
        self._barWidth = barWidth or ((self._length-self.loPad-self.hiPad)/float(self._catCount or 1))
        self._calcTickmarkPositions()
        if self.labelAxisMode == 'axispmv':
            self._pmv = [sum([series[i] for series in multiSeries]) for i in range(self._catCount)]

    def _calcTickmarkPositions(self):
        n = self._catCount
        if self.tickShift:
            self._tickValues = [t+0.5 for t in range(n)]
        else:
            if self.reverseDirection:
                self._tickValues = list(range(-1,n))
            else:
                self._tickValues = list(range(n+1))

    def _scale(self,idx):
        if self.reverseDirection: idx = self._catCount-idx-1
        return idx

    def scale(self, idx):
        "Returns the position and width in drawing units"
        return (self.loScale(idx), self._barWidth)

    def midScale(self, idx):
        "Returns the bar mid position in drawing units"
        return self.loScale(idx) + 0.5*self._barWidth

def _assertYAxis(axis):
    assert axis.isYAxis, "Cannot connect to other axes (%s), but Y- ones." % axis.__class__.__name__
def _assertXAxis(axis):
    assert axis.isXAxis, "Cannot connect to other axes (%s), but X- ones." % axis.__class__.__name__

class _XTicks:
    _tickTweaks = 0 #try 0.25-0.5

    @property
    def actualTickStrokeWidth(self):
        return getattr(self,'tickStrokeWidth',self.strokeWidth)

    @property
    def actualTickStrokeColor(self):
        return getattr(self,'tickStrokeColor',self.strokeColor)

    def _drawTicksInner(self,tU,tD,g):
        itd = getattr(self,'innerTickDraw',None)
        if itd:
            itd(self,tU,tD,g)
        elif tU or tD:
            sW = self.actualTickStrokeWidth
            tW = self._tickTweaks
            if tW:
                if tU and not tD:
                    tD = tW*sW
                elif tD and not tU:
                    tU = tW*sW
            self._makeLines(g,tU,-tD,self.actualTickStrokeColor,sW,self.strokeDashArray,self.strokeLineJoin,self.strokeLineCap,self.strokeMiterLimit)

    def _drawTicks(self,tU,tD,g=None):
        g = g or Group()
        if self.visibleTicks:
            self._drawTicksInner(tU,tD,g)
        return g

    def _drawSubTicks(self,tU,tD,g):
        if getattr(self,'visibleSubTicks',0) and self.subTickNum>0:
            otv = self._calcSubTicks()
            try:
                self._subTicking = 1
                self._drawTicksInner(tU,tD,g)
            finally:
                del self._subTicking
                self._tickValues = otv

    def makeTicks(self):
        yold=self._y
        try:
            self._y = self._labelAxisPos(getattr(self,'tickAxisMode','axis'))
            g = self._drawTicks(self.tickUp,self.tickDown)
            self._drawSubTicks(getattr(self,'subTickHi',0),getattr(self,'subTickLo',0),g)
            return g
        finally:
            self._y = yold

    def _labelAxisPos(self,mode=None):
        axis = self.joinAxis
        if axis:
            mode = mode or self.labelAxisMode
            if mode == 'low':
                return axis._y
            elif mode == 'high':
                return axis._y + axis._length
        return self._y

class _YTicks(_XTicks):

    def _labelAxisPos(self,mode=None):
        axis = self.joinAxis
        if axis:
            mode = mode or self.labelAxisMode
            if mode == 'low':
                return axis._x
            elif mode == 'high':
                return axis._x + axis._length
        return self._x

    def makeTicks(self):
        xold=self._x
        try:
            self._x = self._labelAxisPos(getattr(self,'tickAxisMode','axis'))
            g = self._drawTicks(self.tickRight,self.tickLeft)
            self._drawSubTicks(getattr(self,'subTickHi',0),getattr(self,'subTickLo',0),g)
            return g
        finally:
            self._x = xold

class XCategoryAxis(_XTicks,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._y = yAxis._y
        elif mode == 'top':
            self._y = yAxis._y + yAxis._length
        elif mode == 'value':
            self._y = yAxis.scale(pos)
        elif mode == 'points':
            self._y = pos

    def _joinToAxis(self):
        ja = self.joinAxis
        if ja:
            jam = self.joinAxisMode
            if jam in ('bottom', 'top'):
                self.joinToAxis(ja, mode=jam)
            elif jam in ('value', 'points'):
                self.joinToAxis(ja, mode=jam, pos=self.joinAxisPos)

    def loScale(self, idx):
        """returns the x position in drawing units"""
        return self._x + self.loPad + self._scale(idx)*self._barWidth

    def makeAxis(self):
        g = Group()
        self._joinToAxis()
        if not self.visibleAxis: return g

        axis = Line(self._x-self.loLLen, self._y, self._x + self._length+self.hiLLen, self._y)
        axis.strokeColor = self.strokeColor
        axis.strokeWidth = self.strokeWidth
        axis.strokeDashArray = self.strokeDashArray
        g.add(axis)

        return g

    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
            pmv = self._pmv if self.labelAxisMode=='axispmv' else None

            for i in range(catCount):
                if reverseDirection: ic = catCount-i-1
                else: ic = i
                if ic>=n: continue
                label=i-catCount
                if label in self.labels:
                    label = self.labels[label]
                else:
                    label = self.labels[i]
                if pmv:
                    _dy = label.dy
                    v = label._pmv = pmv[ic]
                    if v<0: _dy *= -2
                else:
                    _dy = 0
                lpf = label.labelPosFrac
                x = _x + (i+lpf) * barWidth
                label.setOrigin(x,_y+_dy)
                label.setText(categoryNames[ic] or '')
                g.add(label)

        return g

class YCategoryAxis(_YTicks,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
        elif mode == 'right':
            self._x = (xAxis._x + xAxis._length) * 1.0
        elif mode == 'value':
            self._x = xAxis.scale(pos) * 1.0
        elif mode == 'points':
            self._x = pos * 1.0

    def _joinToAxis(self):
        ja = self.joinAxis
        if ja:
            jam = self.joinAxisMode
            if jam in ('left', 'right'):
                self.joinToAxis(ja, mode=jam)
            elif jam in ('value', 'points'):
                self.joinToAxis(ja, mode=jam, pos=self.joinAxisPos)

    def loScale(self, idx):
        "Returns the y position in drawing units"
        return self._y + self._scale(idx)*self._barWidth

    def makeAxis(self):
        g = Group()
        self._joinToAxis()
        if not self.visibleAxis: return g

        axis = Line(self._x, self._y-self.loLLen, self._x, self._y + self._length+self.hiLLen)
        axis.strokeColor = self.strokeColor
        axis.strokeWidth = self.strokeWidth
        axis.strokeDashArray = self.strokeDashArray
        g.add(axis)

        return g

    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
            labels = self.labels
            _x = self._labelAxisPos()
            _y = self._y
            pmv = self._pmv if self.labelAxisMode=='axispmv' else None

            for i in range(catCount):
                if reverseDirection: ic = catCount-i-1
                else: ic = i
                if ic>=n: continue
                label=i-catCount
                if label in self.labels:
                    label = self.labels[label]
                else:
                    label = self.labels[i]
                lpf = label.labelPosFrac
                y = _y + (i+lpf) * barWidth
                if pmv:
                    _dx = label.dx
                    v = label._pmv = pmv[ic]
                    if v<0: _dx *= -2
                else:
                    _dx = 0
                label.setOrigin(_x+_dx, y)
                label.setText(categoryNames[ic] or '')
                g.add(label)
        return g

class TickLabeller:
    '''Abstract base class which may be used to indicate a change
    in the call signature for callable label formats
    '''
    def __call__(self,axis,value):
        return 'Abstract class instance called'

#this matches the old python str behaviour
_defaultLabelFormatter = lambda x: '%.12g' % x

# 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.'),
        strokeLineCap = AttrMapValue(OneOf(0,1,2),desc="Line cap 0=butt, 1=round & 2=square"),
        strokeLineJoin = AttrMapValue(OneOf(0,1,2),desc="Line join 0=miter, 1=round & 2=bevel"),
        strokeMiterLimit = AttrMapValue(isNumber,desc="miter limit control miter line joins"),
        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.'),
        gridStrokeLineCap = AttrMapValue(OneOf(0,1,2),desc="Grid Line cap 0=butt, 1=round & 2=square"),
        gridStrokeLineJoin = AttrMapValue(OneOf(0,1,2),desc="Grid Line join 0=miter, 1=round & 2=bevel"),
        gridStrokeMiterLimit = AttrMapValue(isNumber,desc="Grid miter limit control miter line joins"),
        gridStart = AttrMapValue(isNumberOrNone, desc='Start of grid lines wrt axis origin'),
        gridEnd = AttrMapValue(isNumberOrNone, desc='End of grid lines wrt axis origin'),
        drawGridLast = AttrMapValue(isBoolean, desc='if true draw gridlines after everything else.'),
        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.'),
        labelAxisMode = AttrMapValue(OneOf('high','low','axis'), desc="Like joinAxisMode, but for 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.'),
        avoidBoundSpace = AttrMapValue(EitherOr((isNumberOrNone,SequenceOf(isNumber,emptyOK=0,lo=2,hi=2))), desc='Space to allow above and below.'),
        abf_ignore_zero = AttrMapValue(EitherOr((NoneOr(isBoolean),SequenceOf(isBoolean,emptyOK=0,lo=2,hi=2))), desc='Set to True to make the avoidBoundFrac calculations treat zero as non-special'),
        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!"),
        skipEndL = AttrMapValue(OneOf('none','start','end','both'), desc='Skip high/low tick labels'),
        origShiftIPC = AttrMapValue(isNumberOrNone, desc='Lowest label shift interval ratio.'),
        origShiftMin = AttrMapValue(isNumberOrNone, desc='Minimum amount to shift.'),
        origShiftSpecialValue = AttrMapValue(isNumberOrNone, desc='special value for shift'),
        tickAxisMode = AttrMapValue(OneOf('high','low','axis'), desc="Like joinAxisMode, but for the ticks"),
        reverseDirection = AttrMapValue(isBoolean, desc='If true reverse category direction.'),
        annotations = AttrMapValue(None,desc='list of annotations'),
        loLLen = AttrMapValue(isNumber, desc='extra line length before start of the axis'),
        hiLLen = AttrMapValue(isNumber, desc='extra line length after end of the axis'),
        subTickNum = AttrMapValue(isNumber, desc='Number of axis sub ticks, if >0'),
        subTickLo = AttrMapValue(isNumber, desc='sub tick down or left'),
        subTickHi = AttrMapValue(isNumber, desc='sub tick up or right'),
        visibleSubTicks = AttrMapValue(isBoolean, desc='Display axis sub ticks, if true.'),
        visibleSubGrid = AttrMapValue(isBoolean, desc='Display axis sub grid, if true.'),
        subGridStrokeWidth = AttrMapValue(isNumber, desc='Width of grid lines.'),
        subGridStrokeColor = AttrMapValue(isColorOrNone, desc='Color of grid lines.'),
        subGridStrokeDashArray = AttrMapValue(isListOfNumbersOrNone, desc='Dash array used for grid lines.'),
        subGridStrokeLineCap = AttrMapValue(OneOf(0,1,2),desc="Grid Line cap 0=butt, 1=round & 2=square"),
        subGridStrokeLineJoin = AttrMapValue(OneOf(0,1,2),desc="Grid Line join 0=miter, 1=round & 2=bevel"),
        subGridStrokeMiterLimit = AttrMapValue(isNumber,desc="Grid miter limit control miter line joins"),
        subGridStart = AttrMapValue(isNumberOrNone, desc='Start of grid lines wrt axis origin'),
        subGridEnd = AttrMapValue(isNumberOrNone, desc='End of grid lines wrt axis origin'),
        tickStrokeWidth = AttrMapValue(isNumber, desc='Width of ticks if specified.'),
        subTickStrokeWidth = AttrMapValue(isNumber, desc='Width of sub ticks if specified.'),
        subTickStrokeColor = AttrMapValue(isColorOrNone, desc='Color of sub ticks if specified.'),
        tickStrokeColor = AttrMapValue(isColorOrNone, desc='Color of ticks if specified.'),
        keepTickLabelsInside = AttrMapValue(isBoolean, desc='Ensure tick labels do not project beyond bounds of axis if true'),
        skipGrid = AttrMapValue(OneOf('none','top','both','bottom'),"grid lines to skip top bottom both none"),
        requiredRange = AttrMapValue(isNumberOrNone, desc='Minimum required value range.'),
        innerTickDraw = AttrMapValue(isNoneOrCallable, desc="Callable to replace _drawInnerTicks"),
        extraMinMaxValues = AttrMapValue(isListOfNumbersOrNone, desc='extra values to use in min max calculation'),
        )

    def __init__(self,**kw):
        assert self.__class__.__name__!='ValueAxis', 'Abstract Class ValueAxis Instantiated'
        self._setKeywords(**kw)
        self._setKeywords(
                        _configured = 0,
                        # private properties set by methods.  The initial values
                        # here are to make demos easy; they would always be
                        # overridden in real life.
                        _x = 50,
                        _y = 50,
                        _length = 100,

                        # public properties
                        visible = 1,
                        visibleAxis = 1,
                        visibleLabels = 1,
                        visibleTicks = 1,
                        visibleGrid = 0,
                        forceZero = 0,

                        strokeWidth = 1,
                        strokeColor = STATE_DEFAULTS['strokeColor'],
                        strokeDashArray = STATE_DEFAULTS['strokeDashArray'],
                        strokeLineJoin =  STATE_DEFAULTS['strokeLineJoin'],
                        strokeLineCap = STATE_DEFAULTS['strokeLineCap'],
                        strokeMiterLimit = STATE_DEFAULTS['strokeMiterLimit'],
                        gridStrokeWidth = 0.25,
                        gridStrokeColor = STATE_DEFAULTS['strokeColor'],
                        gridStrokeDashArray = STATE_DEFAULTS['strokeDashArray'],
                        gridStrokeLineJoin =  STATE_DEFAULTS['strokeLineJoin'],
                        gridStrokeLineCap = STATE_DEFAULTS['strokeLineCap'],
                        gridStrokeMiterLimit = STATE_DEFAULTS['strokeMiterLimit'],
                        gridStart = None,
                        gridEnd = None,
                        drawGridLast = False,
                        visibleSubGrid = 0,
                        visibleSubTicks = 0,
                        subTickNum = 0,
                        subTickLo = 0,
                        subTickHi = 0,
                        subGridStrokeLineJoin = STATE_DEFAULTS['strokeLineJoin'],
                        subGridStrokeLineCap = STATE_DEFAULTS['strokeLineCap'],
                        subGridStrokeMiterLimit = STATE_DEFAULTS['strokeMiterLimit'],
                        subGridStrokeWidth = 0.25,
                        subGridStrokeColor = STATE_DEFAULTS['strokeColor'],
                        subGridStrokeDashArray = STATE_DEFAULTS['strokeDashArray'],
                        subGridStart = None,
                        subGridEnd = None,
                        labels = TypedPropertyCollection(Label),
                        keepTickLabelsInside = 0,

                        # how close can the ticks be?
                        minimumTickSpacing = 10,
                        maximumTicks = 7,

                        # a format string like '%0.2f'
                        # or a function which takes the value as an argument and returns a string
                        _labelTextFormat = None,
                        labelAxisMode = 'axis',
                        labelTextFormat = None,
                        labelTextPostFormat = None,
                        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.
                        valueMin = None,
                        valueMax = None,
                        valueStep = None,
                        avoidBoundFrac = None,
                        avoidBoundSpace = None,
                        abf_ignore_zero = False,
                        rangeRound = 'none',
                        zrangePref = 0,
                        style = 'normal',
                        skipEndL='none',
                        origShiftIPC = None,
                        origShiftMin = None,
                        origShiftSpecialValue = None,
                        tickAxisMode = 'axis',
                        reverseDirection=0,
                        loLLen=0,
                        hiLLen=0,
                        requiredRange=0,
                        extraMinMaxValues=None,
                        )
        self.labels.angle = 0

    def setPosition(self, x, y, length):
        # ensure floating point
        self._x = float(x)
        self._y = float(y)
        self._length = float(length)

    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._configure_end()

    def _configure_end(self):
        self._calcTickmarkPositions()
        self._calcScaleFactor()
        self._configured = 1

    @lru_cache()
    def _getValueStepAndTicks(self, valueMin, valueMax):
        self._valueMin = valueMin
        self._valueMax = valueMax
        valueStep,T = self._calcStepAndTickPositions()
        return valueStep, T, valueStep*1e-8

    def _preRangeAdjust(self,valueMin,valueMax):
        rr = self.requiredRange
        if rr>0:
            r = valueMax - valueMin
            if r<rr:
                m = 0.5*(valueMax+valueMin)
                rr *= 0.5
                y1 = min(m-rr,valueMin)
                y2 = max(m+rr,valueMax)
                if valueMin>=100 and y1<100:
                    y2 = y2 + 100 - y1
                    y1 = 100
                elif valueMin>=0 and y1<0:
                    y2 = y2 - y1
                    y1 = 0
                valueMin = self._cValueMin = y1
                valueMax = self._cValueMax = y2
        return valueMin,valueMax


    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
        if valueMin is None: valueMin = self._cValueMin = _findMin(dataSeries,self._dataIndex,0, self.extraMinMaxValues)
        if valueMax is None: valueMax = self._cValueMax = _findMax(dataSeries,self._dataIndex,0, self.extraMinMaxValues)
        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,extraMinMaxValues=self.extraMinMaxValues)
            if oMax is None: valueMax = self._cValueMax = _findMax(dataSeries,self._dataIndex,0,special=special,extraMinMaxValues=self.extraMinMaxValues)

        valueMin, valueMax = self._preRangeAdjust(valueMin,valueMax)

        rangeRound = self.rangeRound

        cMin = valueMin
        cMax = valueMax
        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 not isSeq(abf):
            abf = abf, abf
        abfiz = getattr(self,'abf_ignore_zero', False)
        if not isSeq(abfiz):
            abfiz = abfiz, abfiz
        do_rr = rangeRound != 'none' and do_rr
        if do_rr:
            rrn = rangeRound in ['both','floor']
            rrx = rangeRound in ['both','ceiling']
        else:
            rrn = rrx = 0

        abS = self.avoidBoundSpace
        do_abs = abS
        if do_abs:
            if not isSeq(abS):
                abS = abS, abS
        aL = float(self._length)

        go = do_rr or do_abf or do_abs
        iter = 0
        while go and iter<=10:
            iter += 1
            go = 0
            if do_abf or do_abs:
                valueStep, T, fuzz = self._getValueStepAndTicks(valueMin, valueMax)
                if do_abf:
                    i0 = valueStep*abf[0]
                    i1 = valueStep*abf[1]
                else:
                    i0 = i1 = 0
                if do_abs:
                    sf = (valueMax-valueMin)/aL
                    i0 = max(i0,abS[0]*sf)
                    i1 = max(i1,abS[1]*sf)
                if rrn: v = T[0]
                else: v = valueMin
                u = cMin-i0
                if (abfiz[0] or abs(v)>fuzz) and v>=u+fuzz:
                    valueMin = u
                    go = 1
                if rrx: v = T[-1]
                else: v = valueMax
                u = cMax+i1
                if (abfiz[1] or abs(v)>fuzz) and v<=u-fuzz:
                    valueMax = u
                    go = 1

            if do_rr:
                valueStep, T, fuzz = self._getValueStepAndTicks(valueMin, valueMax)
                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]
        if iter and not go:
            self._computedValueStep = valueStep
        else:
            self._computedValueStep = None

        self._valueMin = valueMin
        self._valueMax = valueMax

        origShiftIPC = self.origShiftIPC
        origShiftMin = self.origShiftMin
        if origShiftMin is not None or origShiftIPC is not None:
            origShiftSpecialValue = self.origShiftSpecialValue
            self._calcValueStep()
            valueMax, valueMin = self._valueMax, self._valueMin
            if origShiftSpecialValue is None or abs(origShiftSpecialValue-valueMin)<1e-6:
                if origShiftIPC:
                    m = origShiftIPC*self._valueStep
                else:
                    m = 0
                if origShiftMin:
                    m = max(m,(valueMax-valueMin)*origShiftMin/self._length)
                self._valueMin -= m

        self._rangeAdjust()

    def _pseudo_configure(self):
        self._valueMin = self.valueMin
        self._valueMax = self.valueMax
        if hasattr(self,'_subTickValues'): del self._subTickValues
        self._configure_end()

    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 _calcStepAndTickPositions(self):
        valueStep = getattr(self,'_computedValueStep',None)
        if valueStep:
            del self._computedValueStep
            self._valueStep = valueStep
        else:
            self._calcValueStep()
            valueStep = self._valueStep
        valueMin = self._valueMin
        valueMax = self._valueMax
        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 valueStep,[i*valueStep for i in range(i0,i1+1)]

    def _calcTickPositions(self):
        return self._calcStepAndTickPositions()[1]

    def _calcSubTicks(self):
        if not hasattr(self,'_tickValues'):
            self._pseudo_configure()
        otv = self._tickValues
        if not hasattr(self,'_subTickValues'):
            acn = self.__class__.__name__
            if acn[:11]=='NormalDateX':
                iFuzz = 0
                dCnv = int
            else:
                iFuzz = 1e-8
                dCnv = lambda x:x

            OTV = [tv for tv in otv if getattr(tv,'_doSubTicks',1)]
            T = [].append
            nst = int(self.subTickNum)
            i = len(OTV)
            if i<2:
                self._subTickValues = []
            else:
                if i==2:
                    dst = OTV[1]-OTV[0]
                elif i==3:
                    dst = max(OTV[1]-OTV[0],OTV[2]-OTV[1])
                else:
                    i >>= 1
                    dst = OTV[i+1] - OTV[i]
                fuzz = dst*iFuzz
                vn = self._valueMin+fuzz
                vx = self._valueMax-fuzz
                if OTV[0]>vn: OTV.insert(0,OTV[0]-dst)
                if OTV[-1]<vx: OTV.append(OTV[-1]+dst)
                dst /= float(nst+1)
                for i,x in enumerate(OTV[:-1]):
                    for j in range(nst):
                        t = x+dCnv((j+1)*dst)
                        if t<=vn or t>=vx: continue
                        T(t)
                self._subTickValues = T.__self__
        self._tickValues = self._subTickValues
        return otv

    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 _defaultLabelFormatter)
        elif f is str and self._allIntTicks(): f = '%.0f'
        elif hasattr(f,'calcPlaces'):
            f.calcPlaces(self._tickValues)
        post = self.labelTextPostFormat
        scl = self.labelTextScale
        pos = [self._x, self._y]
        d = self._dataIndex
        pos[1-d] = self._labelAxisPos()
        labels = self.labels
        if self.skipEndL!='none':
            if self.isXAxis:
                sk = self._x
            else:
                sk = self._y
            if self.skipEndL=='start':
                sk = [sk]
            else:
                sk = [sk,sk+self._length]
                if self.skipEndL=='end':
                    del sk[0]
        else:
            sk = []

        nticks = len(self._tickValues)
        nticks1 = nticks - 1
        for i,tick in enumerate(self._tickValues):
            label = i-nticks
            if label in labels:
                label = labels[label]
            else:
                label = labels[i]
            if f and label.visible:
                v = self.scale(tick)
                if sk:
                    for skv in sk:
                        if abs(skv-v)<1e-6:
                            v = None
                            break
                if v is not None:
                    if scl is not None:
                        t = tick*scl
                    else:
                        t = tick
                    if isinstance(f, str): txt = f % t
                    elif isSeq(f):
                        #it's a list, use as many items as we get
                        if i < len(f):
                            txt = f[i]
                        else:
                            txt = ''
                    elif hasattr(f,'__call__'):
                        if isinstance(f,TickLabeller):
                            txt = f(self,t)
                        else:
                            txt = f(t)
                    else:
                        raise ValueError('Invalid labelTextFormat %s' % f)
                    if post: txt = post % txt
                    pos[d] = v
                    label.setOrigin(*pos)
                    label.setText(txt)

                    #special property to ensure a label doesn't project beyond the bounds of an x-axis
                    if self.keepTickLabelsInside: 
                        if isinstance(self, XValueAxis):  #not done yet for y axes
                            a_x = self._x
                            if not i:  #first one
                                x0, y0, x1, y1 = label.getBounds()
                                if x0 < a_x:
                                    label = label.clone(dx=label.dx + a_x - x0)
                            if i==nticks1:  #final one
                                a_x1 = a_x +self._length
                                x0, y0, x1, y1 = label.getBounds()
                                if x1 > a_x1:
                                    label=label.clone(dx=label.dx-x1+a_x1)
                    g.add(label)

        return g

    def scale(self, value):
        """Converts a numeric value to a plotarea position.
        The chart first configures the axis, then asks it to
        """
        assert self._configured, "Axis cannot scale numbers before it is configured"
        if value is None: value = 0

        #this could be made more efficient by moving the definition of org and sf into the configuration
        org = (self._x, self._y)[self._dataIndex]
        sf = self._scaleFactor
        if self.reverseDirection:
            sf = -sf
            org += self._length
        return org + sf*(value - self._valueMin)

class XValueAxis(_XTicks,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,**kw):
        ValueAxis.__init__(self,**kw)

        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._y = yAxis._y * 1.0
        elif mode == 'top':
            self._y = (yAxis._y + yAxis._length) * 1.0
        elif mode == 'value':
            self._y = yAxis.scale(pos) * 1.0
        elif mode == 'points':
            self._y = pos * 1.0

    def _joinToAxis(self):
        ja = self.joinAxis
        if ja:
            jam = self.joinAxisMode or 'bottom'
            if jam in ('bottom', 'top'):
                self.joinToAxis(ja, mode=jam)
            elif jam in ('value', 'points'):
                self.joinToAxis(ja, mode=jam, pos=self.joinAxisPos)

    def makeAxis(self):
        g = Group()
        self._joinToAxis()
        if not self.visibleAxis: return g

        axis = Line(self._x-self.loLLen, self._y, self._x + self._length+self.hiLLen, self._y)
        axis.strokeColor = self.strokeColor
        axis.strokeWidth = self.strokeWidth
        axis.strokeDashArray = self.strokeDashArray
        g.add(axis)

        return g

#additional utilities to help specify calendar dates on which tick marks
#are to be plotted.  After some thought, when the magic algorithm fails,
#we can let them specify a number of days-of-the-year to tick in any given
#year.

#################################################################################
#
#   Preliminary support objects/functions for the axis used in time series charts
#
#################################################################################
_months = ['jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov','dec']
_maxDays = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
def parseDayAndMonth(dmstr):
    """This accepts and validates strings like "31-Dec" i.e. dates
    of no particular year.  29 Feb is allowed.  These can be used
    for recurring dates.  It returns a (dd, mm) pair where mm is the
    month integer.  If the text is not valid it raises an error.
    """

    dstr, mstr = dmstr.split('-')
    dd = int(dstr)
    mstr = mstr.lower()
    mm = _months.index(mstr) + 1
    assert dd <= _maxDays[mm-1]
    return (dd, mm)

class _isListOfDaysAndMonths(Validator):
    """This accepts and validates lists of strings like "31-Dec" i.e. dates
    of no particular year.  29 Feb is allowed.  These can be used
    for recurring dates.
    """
    def test(self,x):
        if isSeq(x):
            answer = True
            for element in x:
                try:
                    dd, mm = parseDayAndMonth(element)
                except:
                    answer = False
            return answer
        else:
            return False

    def normalize(self,x):
        #we store them as presented, it's the most presentable way
        return x

isListOfDaysAndMonths = _isListOfDaysAndMonths()

_NDINTM = 1,2,3,6,12,24,60,120,180,240,300,360,420,480,540,600,720,840,960,1080,1200,2400
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.'),
        forceDatesEachYear = AttrMapValue(isListOfDaysAndMonths, desc='List of dates in format "31-Dec",' +
            '"1-Jan".  If present they will always be used for tick marks in the current year, rather ' +
            'than the dates chosen by the automatic algorithm. Hyphen compulsory, case of month optional.'),
        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.'),
        specifiedTickDates = AttrMapValue(NoneOr(SequenceOf(isNormalDate)), desc='Actual tick values to use; no calculations done'),
        specialTickClear = AttrMapValue(isBoolean, desc='clear rather than delete close ticks when forced first/end dates'),
        skipGrid = AttrMapValue(OneOf('none','top','both','bottom'),"grid lines to skip top bottom both none"),
        )

    _valueClass = normalDate.ND

    def __init__(self,**kw):
        XValueAxis.__init__(self,**kw)

        # some global variables still used...
        self.bottomAxisLabelSlack = 0.1
        self.niceMonth = 1
        self.forceEndDate = 0
        self.forceFirstDate = 0
        self.forceDatesEachYear = []
        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.specialTickClear = 0
        self.valueSteps = self.specifiedTickDates = 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...

        Yes please says Andy :-(.  Modified on 19 June 2006 to attempt to allow
        a mode where one can specify recurring days and months.
        """
        VC = self._valueClass
        axisLength = self._length
        formatter = self._dateFormatter
        if isinstance(formatter,TickLabeller):
            def formatter(tick):
                return self._dateFormatter(self,tick)
        firstDate = xVals[0] if not self.valueMin else VC(self.valueMin)
        endDate = xVals[-1] if not self.valueMax else VC(self.valueMax)
        labels = self.labels
        fontName, fontSize, leading = labels.fontName, labels.fontSize, labels.leading
        textAnchor, boxAnchor, angle = labels.textAnchor, labels.boxAnchor, labels.angle
        RBL = _textBoxLimits(formatter(firstDate).split('\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 or 0,self.minimumTickSpacing)

        W = w+w*self.bottomAxisLabelSlack
        ticks = []
        labels = []
        maximumTicks = self.maximumTicks

        if self.specifiedTickDates:
            ticks = [VC(x) for x in self.specifiedTickDates]
            labels = [formatter(d) for d in ticks]
            if self.forceFirstDate and firstDate==ticks[0] and (axisLength/float(ticks[-1]-ticks[0]))*(ticks[1]-ticks[0])<=W:
                if self.specialTickClear:
                    labels[1] = ''
                else:
                    del ticks[1], labels[1]
            if self.forceEndDate and endDate==ticks[-1] and (axisLength/float(ticks[-1]-ticks[0]))*(ticks[-1]-ticks[-2])<=W:
                if self.specialTickClear:
                    labels[-2] = ''
                else:
                    del ticks[-2], labels[-2]
            return ticks, labels

        #AR 20060619 - first we try the approach where the user has explicitly
        #specified the days of year to be ticked.  Other explicit routes may
        #be added.
        if self.forceDatesEachYear:
            forcedPartialDates = list(map(parseDayAndMonth, self.forceDatesEachYear))
            #generate the list of dates in the range.
            #print 'dates range from %s to %s' % (firstDate, endDate)
            firstYear = firstDate.year()
            lastYear = endDate.year()
            ticks = []
            labels = []
            yyyy = firstYear
            #generate all forced dates between the year it starts and the year it
            #ends, adding them if within range.
            while yyyy <= lastYear:
                for (dd, mm) in forcedPartialDates:
                    theDate = normalDate.ND((yyyy, mm, dd))
                    if theDate >= firstDate and theDate <= endDate:
                        ticks.append(theDate)
                        labels.append(formatter(theDate))
                yyyy += 1

            #first and last may still be forced in.
            if self.forceFirstDate and firstDate!=ticks[0]:
                ticks.insert(0, firstDate)
                labels.insert(0,formatter(firstDate))
                if (axisLength/float(ticks[-1]-ticks[0]))*(ticks[1]-ticks[0])<=W:
                    if self.specialTickClear:
                        labels[1] = ''
                    else:
                        del ticks[1], labels[1]
            if self.forceEndDate and endDate!=ticks[-1]:
                ticks.append(endDate)
                labels.append(formatter(endDate))
                if (axisLength/float(ticks[-1]-ticks[0]))*(ticks[-1]-ticks[-2])<=W:
                    if self.specialTickClear:
                        labels[-2] = ''
                    else:
                        del ticks[-2], labels[-2]

            #print 'xVals found on forced dates =', ticks
            return ticks, labels

        def addTick(i, xVals=xVals, formatter=formatter, ticks=ticks, labels=labels):
            ticks.insert(0,xVals[i])
            labels.insert(0,formatter(xVals[i]))

        n = len(xVals)
        #otherwise, we apply the 'magic algorithm...' which looks for nice spacing
        #based on the size and separation of the labels.
        for d in _NDINTM:
            k = n/d
            if k<=maximumTicks and k*W <= axisLength:
                i = n-1
                if self.niceMonth:
                    j = endDate.month() % (d<=12 and d or 12)
                    if j:
                        if self.forceEndDate:
                            addTick(i)
                            ticks[0]._doSubTicks=0
                        i -= j

                #weird first date ie not at end of month
                try:
                    wfd = firstDate.month() == xVals[1].month()
                except:
                    wfd = 0

                while i>=wfd:
                    addTick(i)
                    i -= d

                if self.forceFirstDate and ticks[0]!=firstDate:
                    addTick(0)
                    ticks[0]._doSubTicks=0
                    if (axisLength/float(ticks[-1]-ticks[0]))*(ticks[1]-ticks[0])<=W:
                        if self.specialTickClear:
                            labels[1] = ''
                        else:
                            del ticks[1], labels[1]
                if self.forceEndDate and self.niceMonth and j:
                    if (axisLength/float(ticks[-1]-ticks[0]))*(ticks[-1]-ticks[-2])<=W:
                        if self.specialTickClear:
                            labels[-2] = ''
                        else:
                            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
        raise ValueError('Problem selecting NormalDate value axis tick positions')

    def _convertXV(self,data):
        '''Convert all XValues to a standard normalDate type'''

        VC = self._valueClass
        for D in data:
            for i in range(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)
        xVals = set()
        for x in data:
            for dv in x:
                xVals.add(dv[0])
        xVals = list(xVals)
        xVals.sort()
        VC = self._valueClass
        steps,labels = self._getStepsAndLabels(xVals)
        valueMin, valueMax = self.valueMin, self.valueMax
        valueMin = xVals[0]  if valueMin is None else VC(valueMin)
        valueMax = xVals[-1] if valueMax is None else VC(valueMax)
        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(_YTicks,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
        elif mode == 'right':
            self._x = (xAxis._x + xAxis._length) * 1.0
        elif mode == 'value':
            self._x = xAxis.scale(pos) * 1.0
        elif mode == 'points':
            self._x = pos * 1.0

    def _joinToAxis(self):
        ja = self.joinAxis
        if ja:
            jam = self.joinAxisMode
            if jam in ('left', 'right'):
                self.joinToAxis(ja, mode=jam)
            elif jam in ('value', 'points'):
                self.joinToAxis(ja, mode=jam, pos=self.joinAxisPos)

    def makeAxis(self):
        g = Group()
        self._joinToAxis()
        if not self.visibleAxis: return g

        axis = Line(self._x, self._y-self.loLLen, self._x, self._y + self._length+self.hiLLen)
        axis.strokeColor = self.strokeColor
        axis.strokeWidth = self.strokeWidth
        axis.strokeDashArray = self.strokeDashArray
        g.add(axis)
        return g

class TimeValueAxis:
    _mc = 60
    _hc = 60*_mc
    _dc = 24*_hc

    def __init__(self,*args,**kwds):
        if not self.labelTextFormat:
            self.labelTextFormat = self.timeLabelTextFormatter
        self._saved_tickInfo = {}

    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))
            #here's where we try to choose the correct value for the unit
            if rawInterval >= self._dc:
                d = self._dc
                self._unit = 'days'
            elif rawInterval >= self._hc:
                d = self._hc
                self._unit = 'hours'
            elif rawInterval >= self._mc:
                d = self._mc
                self._unit = 'minutes'
            else:
                d = 1
                self._unit = 'seconds'
            self._unitd = d
            if d>1:
                rawInterval = int(rawInterval/d)
            self._valueStep = nextRoundNumber(rawInterval) * d
        else:
            self._valueStep = self.valueStep

    def timeLabelTextFormatter(self,val):
        u = self._unitd
        k = (u,tuple(self._tickValues))
        if k in self._saved_tickInfo:
            fmt = self._saved_tickInfo[k]
        else:
            uf = float(u)
            tv = [v/uf for v in self._tickValues]
            s = self._unit[0]
            if _allInt(tv):
                fmt = lambda x, uf=uf, s=s: '%.0f%s' % (x/uf,s)
            else:
                stv = ['%.10f' % v for v in tv]
                stvl = max((len(v.rstrip('0'))-v.index('.')-1) for v in stv)
                if u==1:
                    fmt = lambda x,uf=uf,fmt='%%.%dfs' % stvl: fmt % (x/uf)
                else:
                    #see if we can represent fractions
                    fm = 24 if u==self._dc else 60
                    fv = [(v - int(v))*fm for v in tv]
                    if _allInt(fv):
                        s1 = 'h' if u==self._dc else ('m' if u==self._mc else 's')
                        fmt = lambda x,uf=uf,fm=fm, fmt='%%d%s%%d%%s' % (s,s1): fmt % (int(x/uf),int((x/uf - int(x/uf))*fm))
                    else:
                        fmt = lambda x,uf=uf,fmt='%%.%df%s' % (stvl,s): fmt % (x/uf)
            self._saved_tickInfo[k] = fmt

        return fmt(val)
        


class XTimeValueAxis(TimeValueAxis,XValueAxis):
    def __init__(self,*args,**kwds):
        XValueAxis.__init__(self,*args,**kwds)
        TimeValueAxis.__init__(self,*args,**kwds)

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,
        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'),
        labelVOffset = AttrMapValue(isNumber, desc='add this to the labels'),
        )

    def __init__(self,**kw):
        YValueAxis.__init__(self,**kw)
        self.requiredRange = 30
        self.leftAxisPercent = 1
        self.leftAxisOrigShiftIPC = 0.15
        self.leftAxisOrigShiftMin = 12
        self.leftAxisSkipLL0 = self.labelVOffset = 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 = list(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, labelVOffset=self.labelVOffset)
        abf = self.avoidBoundFrac
        if abf:
            i1 = (T[1]-T[0])
            if not isSeq(abf):
                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, labelVOffset=self.labelVOffset)

        self._valueMin = T[0]
        self._valueMax = T[-1]
        self._tickValues = 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 isSeq(self.leftAxisSkipLL0):
                for x in self.leftAxisSkipLL0:
                    try:
                        L[x] = ''
                    except IndexError:
                        pass
            L[0] = ''

class LogValueAxis(ValueAxis):

    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(
            math_log10(self._valueMax) - math_log10(self._valueMin))
        return self._scaleFactor


    def _setRange(self,dataSeries):
        valueMin = self.valueMin
        valueMax = self.valueMax
        aMin = _findMin(dataSeries,self._dataIndex,0,extraMinMaxValues=self.extraMinMaxValues)
        aMax = _findMax(dataSeries,self._dataIndex,0,extraMinMaxValues=self.extraMinMaxValues)
        if valueMin is None: valueMin = aMin
        if valueMax is None: valueMax = aMax
        if valueMin>valueMax:
            raise ValueError('%s: valueMin=%r should not be greater than valueMax=%r!' % (self.__class__.__name__valueMin, valueMax))
        if valueMin<=0:
            raise ValueError('%s: valueMin=%r negative values are not allowed!' % (self.__class__.__name__,valueMin))
        abS = self.avoidBoundSpace
        if abS:
            lMin = math_log10(aMin)
            lMax = math_log10(aMax)
            if not isSeq(abS): abS = abS, abS
            a0 = abS[0] or 0
            a1 = abS[1] or 0
            L = self._length - (a0 + a1)
            sf = (lMax-lMin)/float(L)
            lMin -= a0*sf
            lMax += a1*sf
            valueMin = min(valueMin,10**lMin)
            valueMax = max(valueMax,10**lMax)
        self._valueMin = valueMin
        self._valueMax = valueMax

    def _calcTickPositions(self):
        #self._calcValueStep()
        valueMin = cMin = math_log10(self._valueMin)
        valueMax = cMax = math_log10(self._valueMax)
        rr = self.rangeRound
        if rr:
            if rr in ('both','ceiling'):
                i = int(valueMax)
                valueMax =  i + 1 if i<valueMax else i
            if rr in ('both','floor'):
                i = int(valueMin)
                valueMin =  i - 1 if i>valueMin else i
                
        T = [].append
        tv = int(valueMin)
        if tv<valueMin: tv += 1
        n = int(valueMax) - tv + 1
        i = max(int(n/self.maximumTicks),1)
        if i*n>self.maximumTicks: i += 1
        self._powerInc = i
        while True:
            if tv>valueMax: break
            if tv>=valueMin: T(10**tv)
            tv += i
        if valueMin!=cMin: self._valueMin = 10**valueMin
        if valueMax!=cMax: self._valueMax = 10**valueMax
        return T.__self__

    def _calcSubTicks(self):
        if not hasattr(self,'_tickValues'):
            self._pseudo_configure()
        otv = self._tickValues
        if not hasattr(self,'_subTickValues'):
            T = [].append
            valueMin = math_log10(self._valueMin)
            valueMax = math_log10(self._valueMax)+1
            tv = round(valueMin)
            i = self._powerInc
            if i==1:
                fac = 10 / float(self.subTickNum)
                start = 1
                if self.subTickNum == 10: start = 2
                while tv < valueMax:
                    for j in range(start,self.subTickNum):
                        v = fac*j*(10**tv)
                        if v > self._valueMin and v < self._valueMax:
                            T(v)
                    tv += i
            else:
                ng = min(self.subTickNum+1,i-1)
                while ng:
                    if (i % ng)==0:
                        i /= ng
                        break
                    ng -= 1
                else:
                    i = 1
                tv = round(valueMin)
                while True:
                    v = 10**tv
                    if v >= self._valueMax: break
                    if v not in otv:
                        T(v)
                    tv += i
            self._subTickValues = T.__self__
        self._tickValues = self._subTickValues
        return otv


class LogAxisTickLabeller(TickLabeller):
    def __call__(self,axis,value):
        e = math_log10(value)
        e = int(e-0.001 if e<0 else e+0.001)
        if e==0: return '1'
        if e==1: return '10'
        return '10<sup>%s</sup>' % e

class LogAxisTickLabellerS(TickLabeller):
    '''simple log axis labeller tries to use integers
    and short forms else exponential format'''
    def __call__(self,axis,value):
        e = math_log10(value)
        p = int(e-0.001 if e<0 else e+0.001)
        if p==0: return '1'
        s = '1'+p*'0' if p>0 else '0.'+(-(1+p)*'0')+'1'
        se = '%.0e' % value
        return se if len(se)<len(s) else s

class LogAxisLabellingSetup:
    def __init__(self):
        if DirectDrawFlowable is not None:
            self.labels = TypedPropertyCollection(XLabel)
            if self._dataIndex==1:
                self.labels.boxAnchor = 'e'
                self.labels.dx = -5
                self.labels.dy = 0
            else:
                self.labels.boxAnchor = 'n'
                self.labels.dx = 0
                self.labels.dy = -5
            self.labelTextFormat = LogAxisTickLabeller()
        else:
            self.labelTextFormat = LogAxisTickLabellerS()

class LogXValueAxis(LogValueAxis,LogAxisLabellingSetup,XValueAxis):
    _attrMap = AttrMap(BASE=XValueAxis)

    def __init__(self):
        XValueAxis.__init__(self)
        LogAxisLabellingSetup.__init__(self)

    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
        if value == 0.:
            return self._x - self._scaleFactor * math_log10(self._valueMin)
        return self._x + self._scaleFactor * (math_log10(value) - math_log10(self._valueMin))

class LogYValueAxis(LogValueAxis,LogAxisLabellingSetup,YValueAxis):
    _attrMap = AttrMap(BASE=YValueAxis)
    def __init__(self):
        YValueAxis.__init__(self)
        LogAxisLabellingSetup.__init__(self)

    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
        if value == 0.:
            return self._y - self._scaleFactor * math_log10(self._valueMin)
        return self._y + self._scaleFactor * (math_log10(value) - math_log10(self._valueMin))
