~openerp-groupes/openobject-server/6.0-fix-setup-windows

« back to all changes in this revision

Viewing changes to bin/reportlab/graphics/charts/axes.py

  • Committer: pinky
  • Date: 2006-12-07 13:41:40 UTC
  • Revision ID: pinky-3f10ee12cea3c4c75cef44ab04ad33ef47432907
New trunk

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#Copyright ReportLab Europe Ltd. 2000-2004
 
2
#see license.txt for license details
 
3
#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/axes.py
 
4
"""Collection of axes for charts.
 
5
 
 
6
The current collection comprises axes for charts using cartesian
 
7
coordinate systems. All axes might have tick marks and labels.
 
8
There are two dichotomies for axes: one of X and Y flavours and
 
9
another of category and value flavours.
 
10
 
 
11
Category axes have an ordering but no metric. They are divided
 
12
into a number of equal-sized buckets. Their tick marks or labels,
 
13
if available, go BETWEEN the buckets, and the labels are placed
 
14
below to/left of the X/Y-axis, respectively.
 
15
 
 
16
  Value axes have an ordering AND metric. They correspond to a nu-
 
17
  meric quantity. Value axis have a real number quantity associated
 
18
  with it. The chart tells it where to go.
 
19
  The most basic axis divides the number line into equal spaces
 
20
  and has tickmarks and labels associated with each; later we
 
21
  will add variants where you can specify the sampling
 
22
  interval.
 
23
 
 
24
The charts using axis tell them where the labels should be placed.
 
25
 
 
26
Axes of complementary X/Y flavours can be connected to each other
 
27
in various ways, i.e. with a specific reference point, like an
 
28
x/value axis to a y/value (or category) axis. In this case the
 
29
connection can be either at the top or bottom of the former or
 
30
at any absolute value (specified in points) or at some value of
 
31
the former axes in its own coordinate system.
 
32
"""
 
33
__version__=''' $Id$ '''
 
34
 
 
35
import string
 
36
from types import FunctionType, StringType, TupleType, ListType
 
37
 
 
38
from reportlab.lib.validators import    isNumber, isNumberOrNone, isListOfStringsOrNone, isListOfNumbers, \
 
39
                                        isListOfNumbersOrNone, isColorOrNone, OneOf, isBoolean, SequenceOf, \
 
40
                                        isString, EitherOr
 
41
from reportlab.lib.attrmap import *
 
42
from reportlab.lib import normalDate
 
43
from reportlab.graphics.shapes import Drawing, Line, PolyLine, Group, STATE_DEFAULTS, _textBoxLimits, _rotatedBoxLimits
 
44
from reportlab.graphics.widgetbase import Widget, TypedPropertyCollection
 
45
from reportlab.graphics.charts.textlabels import Label
 
46
from reportlab.graphics.charts.utils import nextRoundNumber
 
47
 
 
48
 
 
49
# Helpers.
 
50
 
 
51
def _findMinMaxValue(V, x, default, func, special=None):
 
52
    if type(V[0][0]) in (TupleType,ListType):
 
53
        if special:
 
54
            f=lambda T,x=x,special=special,func=func: special(T,x,func)
 
55
        else:
 
56
            f=lambda T,x=x: T[x]
 
57
        V=map(lambda e,f=f: map(f,e),V)
 
58
    V = filter(len,map(lambda x: filter(lambda x: x is not None,x),V))
 
59
    if len(V)==0: return default
 
60
    return func(map(func,V))
 
61
 
 
62
def _findMin(V, x, default,special=None):
 
63
    '''find minimum over V[i][x]'''
 
64
    return _findMinMaxValue(V,x,default,min,special=special)
 
65
 
 
66
def _findMax(V, x, default,special=None):
 
67
    '''find maximum over V[i][x]'''
 
68
    return _findMinMaxValue(V,x,default,max,special=special)
 
69
 
 
70
class _AxisG(Widget):
 
71
    def _get_line_pos(self,v):
 
72
        v = self.scale(v)
 
73
        try:
 
74
            v = v[0]
 
75
        except:
 
76
            pass
 
77
        return v
 
78
 
 
79
    def _cxLine(self,x,start,end):
 
80
        x = self._get_line_pos(x)
 
81
        return Line(x, self._y + start, x, self._y + end)
 
82
 
 
83
    def _cyLine(self,y,start,end):
 
84
        y = self._get_line_pos(y)
 
85
        return Line(self._x + start, y, self._x + end, y)
 
86
 
 
87
    def _cxLine3d(self,x,start,end,_3d_dx,_3d_dy):
 
88
        x = self._get_line_pos(x)
 
89
        y0 = self._y + start
 
90
        y1 = self._y + end
 
91
        y0, y1 = min(y0,y1),max(y0,y1)
 
92
        x1 = x + _3d_dx
 
93
        return PolyLine([x,y0,x1,y0+_3d_dy,x1,y1+_3d_dy],strokeLineJoin=1)
 
94
 
 
95
    def _cyLine3d(self,y,start,end,_3d_dx,_3d_dy):
 
96
        y = self._get_line_pos(y)
 
97
        x0 = self._x + start
 
98
        x1 = self._x + end
 
99
        x0, x1 = min(x0,x1),max(x0,x1)
 
100
        y1 = y + _3d_dy
 
101
        return PolyLine([x0,y,x0+_3d_dx,y1,x1+_3d_dx,y1],strokeLineJoin=1)
 
102
 
 
103
    def _getLineFunc(self, start, end, parent=None):
 
104
        _3d_dx = getattr(parent,'_3d_dx',None)
 
105
        if _3d_dx is not None:
 
106
            _3d_dy = getattr(parent,'_3d_dy',None)
 
107
            f = self._dataIndex and self._cyLine3d or self._cxLine3d
 
108
            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)
 
109
        else:
 
110
            f = self._dataIndex and self._cyLine or self._cxLine
 
111
            return lambda v, s=start, e=end, f=f: f(v,s,e)
 
112
 
 
113
    def _makeLines(self,g,start,end,strokeColor,strokeWidth,strokeDashArray,parent=None):
 
114
        func = self._getLineFunc(start,end,parent)
 
115
        for t in self._tickValues:
 
116
                L = func(t)
 
117
                L.strokeColor = strokeColor
 
118
                L.strokeWidth = strokeWidth
 
119
                L.strokeDashArray = strokeDashArray
 
120
                g.add(L)
 
121
 
 
122
    def makeGrid(self,g,dim=None,parent=None):
 
123
        '''this is only called by a container object'''
 
124
        s = self.gridStart
 
125
        e = self.gridEnd
 
126
        if dim:
 
127
            s = s is None and dim[0]
 
128
            e = e is None and dim[0]+dim[1]
 
129
        c = self.gridStrokeColor
 
130
        if self.visibleGrid and (s or e) and c is not None:
 
131
            if self._dataIndex: offs = self._x
 
132
            else: offs = self._y
 
133
            self._makeLines(g,s-offs,e-offs,c,self.gridStrokeWidth,self.gridStrokeDashArray,parent=parent)
 
134
 
 
135
# Category axes.
 
136
class CategoryAxis(_AxisG):
 
137
    "Abstract category axis, unusable in itself."
 
138
    _nodoc = 1
 
139
    _attrMap = AttrMap(
 
140
        visible = AttrMapValue(isBoolean, desc='Display entire object, if true.'),
 
141
        visibleAxis = AttrMapValue(isBoolean, desc='Display axis line, if true.'),
 
142
        visibleTicks = AttrMapValue(isBoolean, desc='Display axis ticks, if true.'),
 
143
        visibleLabels = AttrMapValue(isBoolean, desc='Display axis labels, if true.'),
 
144
        visibleGrid = AttrMapValue(isBoolean, desc='Display axis grid, if true.'),
 
145
        strokeWidth = AttrMapValue(isNumber, desc='Width of axis line and ticks.'),
 
146
        strokeColor = AttrMapValue(isColorOrNone, desc='Color of axis line and ticks.'),
 
147
        strokeDashArray = AttrMapValue(isListOfNumbersOrNone, desc='Dash array used for axis line.'),
 
148
        gridStrokeWidth = AttrMapValue(isNumber, desc='Width of grid lines.'),
 
149
        gridStrokeColor = AttrMapValue(isColorOrNone, desc='Color of grid lines.'),
 
150
        gridStrokeDashArray = AttrMapValue(isListOfNumbersOrNone, desc='Dash array used for grid lines.'),
 
151
        gridStart = AttrMapValue(isNumberOrNone, desc='Start of grid lines wrt axis origin'),
 
152
        gridEnd = AttrMapValue(isNumberOrNone, desc='End of grid lines wrt axis origin'),
 
153
        labels = AttrMapValue(None, desc='Handle of the axis labels.'),
 
154
        categoryNames = AttrMapValue(isListOfStringsOrNone, desc='List of category names.'),
 
155
        joinAxis = AttrMapValue(None, desc='Join both axes if true.'),
 
156
        joinAxisPos = AttrMapValue(isNumberOrNone, desc='Position at which to join with other axis.'),
 
157
        reverseDirection = AttrMapValue(isBoolean, desc='If true reverse category direction.'),
 
158
        style = AttrMapValue(OneOf('parallel','stacked','parallel_3d'),"How common category bars are plotted"),
 
159
        labelAxisMode = AttrMapValue(OneOf('high','low','axis'), desc="Like joinAxisMode, but for the axis labels"),
 
160
        )
 
161
 
 
162
    def __init__(self):
 
163
        assert self.__class__.__name__!='CategoryAxis', "Abstract Class CategoryAxis Instantiated"
 
164
        # private properties set by methods.  The initial values
 
165
        # here are to make demos easy; they would always be
 
166
        # overridden in real life.
 
167
        self._x = 50
 
168
        self._y = 50
 
169
        self._length = 100
 
170
        self._catCount = 0
 
171
 
 
172
        # public properties
 
173
        self.visible = 1
 
174
        self.visibleAxis = 1
 
175
        self.visibleTicks = 1
 
176
        self.visibleLabels = 1
 
177
        self.visibleGrid = 0
 
178
 
 
179
        self.strokeWidth = 1
 
180
        self.strokeColor = STATE_DEFAULTS['strokeColor']
 
181
        self.strokeDashArray = STATE_DEFAULTS['strokeDashArray']
 
182
        self.gridStrokeWidth = 0.25
 
183
        self.gridStrokeColor = STATE_DEFAULTS['strokeColor']
 
184
        self.gridStrokeDashArray = STATE_DEFAULTS['strokeDashArray']
 
185
        self.gridStart = self.gridEnd = 0
 
186
        self.labels = TypedPropertyCollection(Label)
 
187
        # if None, they don't get labels. If provided,
 
188
        # you need one name per data point and they are
 
189
        # used for label text.
 
190
        self.categoryNames = None
 
191
        self.joinAxis = None
 
192
        self.joinAxisPos = None
 
193
        self.joinAxisMode = None
 
194
        self.labelAxisMode = 'axis'
 
195
        self.reverseDirection = 0
 
196
        self.style = 'parallel'
 
197
 
 
198
        #various private things which need to be initialized
 
199
        self._labelTextFormat = None
 
200
 
 
201
    def setPosition(self, x, y, length):
 
202
        # ensure floating point
 
203
        self._x = x
 
204
        self._y = y
 
205
        self._length = length
 
206
 
 
207
 
 
208
    def configure(self, multiSeries,barWidth=None):
 
209
        self._catCount = max(map(len,multiSeries))
 
210
        self._barWidth = barWidth or (self._length/float(self._catCount or 1))
 
211
        self._calcTickmarkPositions()
 
212
 
 
213
    def _calcTickmarkPositions(self):
 
214
        self._tickValues = range(self._catCount+1)
 
215
 
 
216
    def draw(self):
 
217
        g = Group()
 
218
 
 
219
        if not self.visible:
 
220
            return g
 
221
 
 
222
        g.add(self.makeAxis())
 
223
        g.add(self.makeTicks())
 
224
        g.add(self.makeTickLabels())
 
225
 
 
226
        return g
 
227
 
 
228
    def _scale(self,idx):
 
229
        if self.reverseDirection: idx = self._catCount-idx-1
 
230
        return idx
 
231
 
 
232
def _assertYAxis(axis):
 
233
    acn = axis.__class__.__name__
 
234
    assert acn[0]=='Y' or acn[:4]=='AdjY', "Cannot connect to other axes (%s), but Y- ones." % acn
 
235
def _assertXAxis(axis):
 
236
    acn = axis.__class__.__name__
 
237
    assert acn[0]=='X' or acn[:11]=='NormalDateX', "Cannot connect to other axes (%s), but X- ones." % acn
 
238
 
 
239
class XCategoryAxis(CategoryAxis):
 
240
    "X/category axis"
 
241
 
 
242
    _attrMap = AttrMap(BASE=CategoryAxis,
 
243
        tickUp = AttrMapValue(isNumber,
 
244
            desc='Tick length up the axis.'),
 
245
        tickDown = AttrMapValue(isNumber,
 
246
            desc='Tick length down the axis.'),
 
247
        joinAxisMode = AttrMapValue(OneOf('bottom', 'top', 'value', 'points', None),
 
248
            desc="Mode used for connecting axis ('bottom', 'top', 'value', 'points', None)."),
 
249
        )
 
250
 
 
251
    _dataIndex = 0
 
252
 
 
253
    def __init__(self):
 
254
        CategoryAxis.__init__(self)
 
255
        self.labels.boxAnchor = 'n' #north - top edge
 
256
        self.labels.dy = -5
 
257
        # ultra-simple tick marks for now go between categories
 
258
        # and have same line style as axis - need more
 
259
        self.tickUp = 0  # how far into chart does tick go?
 
260
        self.tickDown = 5  # how far below axis does tick go?
 
261
 
 
262
 
 
263
    def demo(self):
 
264
        self.setPosition(30, 70, 140)
 
265
        self.configure([(10,20,30,40,50)])
 
266
 
 
267
        self.categoryNames = ['One','Two','Three','Four','Five']
 
268
        # all labels top-centre aligned apart from the last
 
269
        self.labels.boxAnchor = 'n'
 
270
        self.labels[4].boxAnchor = 'e'
 
271
        self.labels[4].angle = 90
 
272
 
 
273
        d = Drawing(200, 100)
 
274
        d.add(self)
 
275
        return d
 
276
 
 
277
 
 
278
    def joinToAxis(self, yAxis, mode='bottom', pos=None):
 
279
        "Join with y-axis using some mode."
 
280
 
 
281
        _assertYAxis(yAxis)
 
282
        if mode == 'bottom':
 
283
            self._x = yAxis._x
 
284
            self._y = yAxis._y
 
285
        elif mode == 'top':
 
286
            self._x = yAxis._x
 
287
            self._y = yAxis._y + yAxis._length
 
288
        elif mode == 'value':
 
289
            self._x = yAxis._x
 
290
            self._y = yAxis.scale(pos)
 
291
        elif mode == 'points':
 
292
            self._x = yAxis._x
 
293
            self._y = pos
 
294
 
 
295
    def scale(self, idx):
 
296
        """returns the x position and width in drawing units of the slice"""
 
297
        return (self._x + self._scale(idx)*self._barWidth, self._barWidth)
 
298
 
 
299
    def makeAxis(self):
 
300
        g = Group()
 
301
 
 
302
 
 
303
        ja = self.joinAxis
 
304
        if ja:
 
305
            jam = self.joinAxisMode
 
306
            jap = self.joinAxisPos
 
307
            jta = self.joinToAxis
 
308
            if jam in ('bottom', 'top'):
 
309
                jta(ja, mode=jam)
 
310
            elif jam in ('value', 'points'):
 
311
                jta(ja, mode=jam, pos=jap)
 
312
 
 
313
        if not self.visibleAxis: return g
 
314
 
 
315
        axis = Line(self._x, self._y, self._x + self._length, self._y)
 
316
        axis.strokeColor = self.strokeColor
 
317
        axis.strokeWidth = self.strokeWidth
 
318
        axis.strokeDashArray = self.strokeDashArray
 
319
        g.add(axis)
 
320
 
 
321
        return g
 
322
 
 
323
    def makeTicks(self):
 
324
        g = Group()
 
325
        if not self.visibleTicks: return g
 
326
        if self.tickUp or self.tickDown:
 
327
            self._makeLines(g,self.tickUp,-self.tickDown,self.strokeColor,self.strokeWidth,self.strokeDashArray)
 
328
        return g
 
329
 
 
330
    def _labelAxisPos(self):
 
331
        axis = self.joinAxis
 
332
        if axis:
 
333
            mode = self.labelAxisMode
 
334
            if mode == 'low':
 
335
                return axis._y
 
336
            elif mode == 'high':
 
337
                return axis._y + axis._length
 
338
        return self._y
 
339
 
 
340
    def makeTickLabels(self):
 
341
        g = Group()
 
342
 
 
343
        if not self.visibleLabels: return g
 
344
 
 
345
        if not (self.categoryNames is None):
 
346
            catCount = self._catCount
 
347
            assert len(self.categoryNames) == catCount, \
 
348
                "expected %d category names but found %d in axis. \n categories = %s" % (
 
349
                len(self.categoryNames), catCount, repr(self.categoryNames)
 
350
                )
 
351
            reverseDirection = self.reverseDirection
 
352
            barWidth = self._barWidth
 
353
            _y = self._labelAxisPos()
 
354
            _x = self._x
 
355
 
 
356
            for i in range(catCount):
 
357
                x = _x + (i+0.5) * barWidth
 
358
                label = self.labels[i]
 
359
                label.setOrigin(x, _y)
 
360
                if reverseDirection: i = catCount-i-1
 
361
                label.setText(self.categoryNames[i])
 
362
                g.add(label)
 
363
 
 
364
        return g
 
365
 
 
366
 
 
367
class YCategoryAxis(CategoryAxis):
 
368
    "Y/category axis"
 
369
 
 
370
    _attrMap = AttrMap(BASE=CategoryAxis,
 
371
        tickLeft = AttrMapValue(isNumber,
 
372
            desc='Tick length left of the axis.'),
 
373
        tickRight = AttrMapValue(isNumber,
 
374
            desc='Tick length right of the axis.'),
 
375
        joinAxisMode = AttrMapValue(OneOf(('left', 'right', 'value', 'points', None)),
 
376
            desc="Mode used for connecting axis ('left', 'right', 'value', 'points', None)."),
 
377
        )
 
378
 
 
379
    _dataIndex = 1
 
380
 
 
381
 
 
382
    def __init__(self):
 
383
        CategoryAxis.__init__(self)
 
384
        self.labels.boxAnchor = 'e' #east - right edge
 
385
        self.labels.dx = -5
 
386
        # ultra-simple tick marks for now go between categories
 
387
        # and have same line style as axis - need more
 
388
        self.tickLeft = 5  # how far left of axis does tick go?
 
389
        self.tickRight = 0  # how far right of axis does tick go?
 
390
 
 
391
 
 
392
    def demo(self):
 
393
        self.setPosition(50, 10, 80)
 
394
        self.configure([(10,20,30)])
 
395
        self.categoryNames = ['One','Two','Three']
 
396
        # all labels top-centre aligned apart from the last
 
397
        self.labels.boxAnchor = 'e'
 
398
        self.labels[2].boxAnchor = 's'
 
399
        self.labels[2].angle = 90
 
400
 
 
401
        d = Drawing(200, 100)
 
402
        d.add(self)
 
403
        return d
 
404
 
 
405
 
 
406
    def joinToAxis(self, xAxis, mode='left', pos=None):
 
407
        "Join with x-axis using some mode."
 
408
 
 
409
        _assertXAxis(xAxis)
 
410
 
 
411
        if mode == 'left':
 
412
            self._x = xAxis._x * 1.0
 
413
            self._y = xAxis._y * 1.0
 
414
        elif mode == 'right':
 
415
            self._x = (xAxis._x + xAxis._length) * 1.0
 
416
            self._y = xAxis._y * 1.0
 
417
        elif mode == 'value':
 
418
            self._x = xAxis.scale(pos) * 1.0
 
419
            self._y = xAxis._y * 1.0
 
420
        elif mode == 'points':
 
421
            self._x = pos * 1.0
 
422
            self._y = xAxis._y * 1.0
 
423
 
 
424
    def scale(self, idx):
 
425
        "Returns the y position and width in drawing units of the slice."
 
426
        return (self._y + self._scale(idx)*self._barWidth, self._barWidth)
 
427
 
 
428
    def makeAxis(self):
 
429
        g = Group()
 
430
 
 
431
        if not self.visibleAxis:
 
432
            return g
 
433
 
 
434
        ja = self.joinAxis
 
435
        if ja:
 
436
            jam = self.joinAxisMode
 
437
            jap = self.joinAxisPos
 
438
            jta = self.joinToAxis
 
439
            if jam in ('left', 'right'):
 
440
                jta(ja, mode=jam)
 
441
            elif jam in ('value', 'points'):
 
442
                jta(ja, mode=jam, pos=jap)
 
443
 
 
444
        axis = Line(self._x, self._y, self._x, self._y + self._length)
 
445
        axis.strokeColor = self.strokeColor
 
446
        axis.strokeWidth = self.strokeWidth
 
447
        axis.strokeDashArray = self.strokeDashArray
 
448
        g.add(axis)
 
449
 
 
450
        return g
 
451
 
 
452
    def makeTicks(self):
 
453
        g = Group()
 
454
        if not self.visibleTicks: return g
 
455
        if self.tickLeft or self.tickRight:
 
456
            self._makeLines(g,-self.tickLeft,self.tickRight,self.strokeColor,self.strokeWidth,self.strokeDashArray)
 
457
        return g
 
458
 
 
459
    def _labelAxisPos(self):
 
460
        axis = self.joinAxis
 
461
        if axis:
 
462
            mode = self.labelAxisMode
 
463
            if mode == 'low':
 
464
                return axis._x
 
465
            elif mode == 'high':
 
466
                return axis._x + axis._length
 
467
        return self._x
 
468
 
 
469
    def makeTickLabels(self):
 
470
        g = Group()
 
471
 
 
472
        if not self.visibleTicks:
 
473
            return g
 
474
 
 
475
        if not (self.categoryNames is None):
 
476
            catCount = self._catCount
 
477
            assert len(self.categoryNames) == catCount, \
 
478
                "expected %d category names but found %d in axis" % (
 
479
                len(self.categoryNames), catCount
 
480
                )
 
481
            reverseDirection = self.reverseDirection
 
482
            barWidth = self._barWidth
 
483
            labels = self.labels
 
484
            _x = self._labelAxisPos()
 
485
            _y = self._y
 
486
            for i in range(catCount):
 
487
                y = _y + (i+0.5) * barWidth
 
488
                label = labels[i]
 
489
                label.setOrigin(_x, y)
 
490
                if reverseDirection: i = catCount-i-1
 
491
                label.setText(self.categoryNames[i])
 
492
                g.add(label)
 
493
 
 
494
        return g
 
495
 
 
496
 
 
497
# Value axes.
 
498
class ValueAxis(_AxisG):
 
499
    "Abstract value axis, unusable in itself."
 
500
 
 
501
    _attrMap = AttrMap(
 
502
        forceZero = AttrMapValue(EitherOr((isBoolean,OneOf('near'))), desc='Ensure zero in range if true.'),
 
503
        visible = AttrMapValue(isBoolean, desc='Display entire object, if true.'),
 
504
        visibleAxis = AttrMapValue(isBoolean, desc='Display axis line, if true.'),
 
505
        visibleLabels = AttrMapValue(isBoolean, desc='Display axis labels, if true.'),
 
506
        visibleTicks = AttrMapValue(isBoolean, desc='Display axis ticks, if true.'),
 
507
        visibleGrid = AttrMapValue(isBoolean, desc='Display axis grid, if true.'),
 
508
        strokeWidth = AttrMapValue(isNumber, desc='Width of axis line and ticks.'),
 
509
        strokeColor = AttrMapValue(isColorOrNone, desc='Color of axis line and ticks.'),
 
510
        strokeDashArray = AttrMapValue(isListOfNumbersOrNone, desc='Dash array used for axis line.'),
 
511
        gridStrokeWidth = AttrMapValue(isNumber, desc='Width of grid lines.'),
 
512
        gridStrokeColor = AttrMapValue(isColorOrNone, desc='Color of grid lines.'),
 
513
        gridStrokeDashArray = AttrMapValue(isListOfNumbersOrNone, desc='Dash array used for grid lines.'),
 
514
        gridStart = AttrMapValue(isNumberOrNone, desc='Start of grid lines wrt axis origin'),
 
515
        gridEnd = AttrMapValue(isNumberOrNone, desc='End of grid lines wrt axis origin'),
 
516
        minimumTickSpacing = AttrMapValue(isNumber, desc='Minimum value for distance between ticks.'),
 
517
        maximumTicks = AttrMapValue(isNumber, desc='Maximum number of ticks.'),
 
518
        labels = AttrMapValue(None, desc='Handle of the axis labels.'),
 
519
        labelTextFormat = AttrMapValue(None, desc='Formatting string or function used for axis labels.'),
 
520
        labelTextPostFormat = AttrMapValue(None, desc='Extra Formatting string.'),
 
521
        labelTextScale = AttrMapValue(isNumberOrNone, desc='Scaling for label tick values.'),
 
522
        valueMin = AttrMapValue(isNumberOrNone, desc='Minimum value on axis.'),
 
523
        valueMax = AttrMapValue(isNumberOrNone, desc='Maximum value on axis.'),
 
524
        valueStep = AttrMapValue(isNumberOrNone, desc='Step size used between ticks.'),
 
525
        valueSteps = AttrMapValue(isListOfNumbersOrNone, desc='List of step sizes used between ticks.'),
 
526
        avoidBoundFrac = AttrMapValue(EitherOr((isNumberOrNone,SequenceOf(isNumber,emptyOK=0,lo=2,hi=2))), desc='Fraction of interval to allow above and below.'),
 
527
        rangeRound=AttrMapValue(OneOf('none','both','ceiling','floor'),'How to round the axis limits'),
 
528
        zrangePref = AttrMapValue(isNumberOrNone, desc='Zero range axis limit preference.'),
 
529
        style = AttrMapValue(OneOf('normal','stacked','parallel_3d'),"How values are plotted!"),
 
530
        )
 
531
 
 
532
    def __init__(self):
 
533
        assert self.__class__.__name__!='ValueAxis', 'Abstract Class ValueAxis Instantiated'
 
534
        self._configured = 0
 
535
        # private properties set by methods.  The initial values
 
536
        # here are to make demos easy; they would always be
 
537
        # overridden in real life.
 
538
        self._x = 50
 
539
        self._y = 50
 
540
        self._length = 100
 
541
 
 
542
        # public properties
 
543
        self.visible = 1
 
544
        self.visibleAxis = 1
 
545
        self.visibleLabels = 1
 
546
        self.visibleTicks = 1
 
547
        self.visibleGrid = 0
 
548
        self.forceZero = 0
 
549
 
 
550
        self.strokeWidth = 1
 
551
        self.strokeColor = STATE_DEFAULTS['strokeColor']
 
552
        self.strokeDashArray = STATE_DEFAULTS['strokeDashArray']
 
553
        self.gridStrokeWidth = 0.25
 
554
        self.gridStrokeColor = STATE_DEFAULTS['strokeColor']
 
555
        self.gridStrokeDashArray = STATE_DEFAULTS['strokeDashArray']
 
556
        self.gridStart = self.gridEnd = 0
 
557
 
 
558
        self.labels = TypedPropertyCollection(Label)
 
559
        self.labels.angle = 0
 
560
 
 
561
        # how close can the ticks be?
 
562
        self.minimumTickSpacing = 10
 
563
        self.maximumTicks = 7
 
564
 
 
565
        # a format string like '%0.2f'
 
566
        # or a function which takes the value as an argument and returns a string
 
567
        self._labelTextFormat = self.labelTextFormat = self.labelTextPostFormat = self.labelTextScale = None
 
568
 
 
569
        # if set to None, these will be worked out for you.
 
570
        # if you override any or all of them, your values
 
571
        # will be used.
 
572
        self.valueMin = None
 
573
        self.valueMax = None
 
574
        self.valueStep = None
 
575
        self.avoidBoundFrac = None
 
576
        self.rangeRound = 'none'
 
577
        self.zrangePref = 0
 
578
        self.style = 'normal'
 
579
 
 
580
    def setPosition(self, x, y, length):
 
581
        # ensure floating point
 
582
        self._x = x * 1.0
 
583
        self._y = y * 1.0
 
584
        self._length = length * 1.0
 
585
 
 
586
    def configure(self, dataSeries):
 
587
        """Let the axis configure its scale and range based on the data.
 
588
 
 
589
        Called after setPosition. Let it look at a list of lists of
 
590
        numbers determine the tick mark intervals.  If valueMin,
 
591
        valueMax and valueStep are configured then it
 
592
        will use them; if any of them are set to None it
 
593
        will look at the data and make some sensible decision.
 
594
        You may override this to build custom axes with
 
595
        irregular intervals.  It creates an internal
 
596
        variable self._values, which is a list of numbers
 
597
        to use in plotting.
 
598
        """
 
599
        self._setRange(dataSeries)
 
600
        self._calcTickmarkPositions()
 
601
        self._calcScaleFactor()
 
602
        self._configured = 1
 
603
 
 
604
    def _getValueStepAndTicks(self, valueMin, valueMax,cache={}):
 
605
        try:
 
606
            K = (valueMin,valueMax)
 
607
            r = cache[K]
 
608
        except:
 
609
            self._valueMin = valueMin
 
610
            self._valueMax = valueMax
 
611
            T = self._calcTickPositions()
 
612
            if len(T)>1:
 
613
                valueStep = T[1]-T[0]
 
614
            else:
 
615
                oVS = self.valueStep
 
616
                self.valueStep = None
 
617
                T = self._calcTickPositions()
 
618
                self.valueStep = oVS
 
619
                if len(T)>1:
 
620
                    valueStep = T[1]-T[0]
 
621
                else:
 
622
                    valueStep = self._valueStep
 
623
            r = cache[K] = valueStep, T, valueStep*1e-8
 
624
        return r
 
625
 
 
626
    def _setRange(self, dataSeries):
 
627
        """Set minimum and maximum axis values.
 
628
 
 
629
        The dataSeries argument is assumed to be a list of data
 
630
        vectors. Each vector is itself a list or tuple of numbers.
 
631
 
 
632
        Returns a min, max tuple.
 
633
        """
 
634
 
 
635
        oMin = valueMin = self.valueMin
 
636
        oMax = valueMax = self.valueMax
 
637
        rangeRound = self.rangeRound
 
638
        if valueMin is None: valueMin = self._cValueMin = _findMin(dataSeries,self._dataIndex,0)
 
639
        if valueMax is None: valueMax = self._cValueMax = _findMax(dataSeries,self._dataIndex,0)
 
640
        if valueMin == valueMax:
 
641
            if valueMax==0:
 
642
                if oMin is None and oMax is None:
 
643
                    zrp = getattr(self,'zrangePref',0)
 
644
                    if zrp>0:
 
645
                        valueMax = zrp
 
646
                        valueMin = 0
 
647
                    elif zrp<0:
 
648
                        valueMax = 0
 
649
                        valueMin = zrp
 
650
                    else:
 
651
                        valueMax = 0.01
 
652
                        valueMin = -0.01
 
653
                elif self.valueMin is None:
 
654
                    valueMin = -0.01
 
655
                else:
 
656
                    valueMax = 0.01
 
657
            else:
 
658
                if valueMax>0:
 
659
                    valueMax = 1.2*valueMax
 
660
                    valueMin = 0.0
 
661
                else:
 
662
                    valueMax = 0.0
 
663
                    valueMin = 1.2*valueMin
 
664
 
 
665
        if getattr(self,'_bubblePlot',None):
 
666
            bubbleMax = float(_findMax(dataSeries,2,0))
 
667
            frac=.25
 
668
            bubbleV=frac*(valueMax-valueMin)
 
669
            self._bubbleV = bubbleV
 
670
            self._bubbleMax = bubbleMax
 
671
            self._bubbleRadius = frac*self._length
 
672
            def special(T,x,func,bubbleV=bubbleV,bubbleMax=bubbleMax):
 
673
                try:
 
674
                    v = T[2]
 
675
                except IndexError:
 
676
                    v = bubbleMAx*0.1
 
677
                bubbleV *= (v/bubbleMax)**0.5
 
678
                return func(T[x]+bubbleV,T[x]-bubbleV)
 
679
            if oMin is None: valueMin = self._cValueMin = _findMin(dataSeries,self._dataIndex,0,special=special)
 
680
            if oMax is None: valueMax = self._cValueMax = _findMax(dataSeries,self._dataIndex,0,special=special)
 
681
 
 
682
        forceZero = self.forceZero
 
683
        if forceZero:
 
684
            if forceZero=='near':
 
685
                forceZero = min(abs(valueMin),abs(valueMax)) <= 5*(valueMax-valueMin)
 
686
            if forceZero:
 
687
                if valueMax<0: valueMax=0
 
688
                elif valueMin>0: valueMin = 0
 
689
 
 
690
        abf = self.avoidBoundFrac
 
691
        do_rr = not getattr(self,'valueSteps',None)
 
692
        do_abf = abf and do_rr
 
693
        if type(abf) not in (TupleType,ListType):
 
694
            abf = abf, abf
 
695
        do_rr = rangeRound is not 'none' and do_rr
 
696
        if do_rr:
 
697
            rrn = rangeRound in ['both','floor']
 
698
            rrx = rangeRound in ['both','ceiling']
 
699
        else:
 
700
            rrn = rrx = 0
 
701
 
 
702
        go = do_rr or do_abf
 
703
        cache = {}
 
704
        cMin = valueMin
 
705
        cMax = valueMax
 
706
        while go:
 
707
            go = 0
 
708
            if do_abf:
 
709
                valueStep, T, fuzz = self._getValueStepAndTicks(valueMin, valueMax, cache)
 
710
                fuzz = 1e-8*valueStep
 
711
                i0 = valueStep*abf[0]
 
712
                i1 = valueStep*abf[1]
 
713
                if rrn: v = T[0]
 
714
                else: v = valueMin
 
715
                d = v - (cMin-i0)
 
716
                if abs(v)>fuzz and fuzz < d:
 
717
                    valueMin = valueMin - (d+fuzz)
 
718
                    go = 1
 
719
                if rrn: v = T[-1]
 
720
                else: v = valueMax
 
721
                d = i1 + cMax - v
 
722
                if abs(v)>fuzz and fuzz < d:
 
723
                    valueMax = valueMax + (d+fuzz)
 
724
                    go = 1
 
725
 
 
726
            if do_rr:
 
727
                valueStep, T, fuzz = self._getValueStepAndTicks(valueMin, valueMax, cache)
 
728
                if rangeRound in ['both','floor'] and valueMin<T[0]-fuzz:
 
729
                    valueMin = T[0]-valueStep
 
730
                    go = 1
 
731
                if rangeRound in ['both','ceiling'] and valueMax>T[-1]+fuzz:
 
732
                    valueMax = T[-1]+valueStep
 
733
                    go = 1
 
734
 
 
735
        self._valueMin, self._valueMax = valueMin, valueMax
 
736
        self._rangeAdjust()
 
737
 
 
738
    def _rangeAdjust(self):
 
739
        """Override this if you want to alter the calculated range.
 
740
 
 
741
        E.g. if want a minumamum range of 30% or don't want 100%
 
742
        as the first point.
 
743
        """
 
744
        pass
 
745
 
 
746
    def _adjustAxisTicks(self):
 
747
        '''Override if you want to put slack at the ends of the axis
 
748
        eg if you don't want the last tick to be at the bottom etc
 
749
        '''
 
750
        pass
 
751
 
 
752
    def _calcScaleFactor(self):
 
753
        """Calculate the axis' scale factor.
 
754
        This should be called only *after* the axis' range is set.
 
755
        Returns a number.
 
756
        """
 
757
        self._scaleFactor = self._length / float(self._valueMax - self._valueMin)
 
758
        return self._scaleFactor
 
759
 
 
760
    def _calcTickPositions(self):
 
761
        self._calcValueStep()
 
762
        P = []
 
763
        valueMin, valueMax, valueStep = self._valueMin, self._valueMax, self._valueStep
 
764
        fuzz = 1e-8*valueStep
 
765
        t = int((valueMin-fuzz)/valueStep) * valueStep
 
766
        if t >= valueMin-fuzz: P.append(t)
 
767
        t = t + valueStep
 
768
        while t <= valueMax+fuzz:
 
769
            P.append(t)
 
770
            t = t + valueStep
 
771
        return P
 
772
 
 
773
    def _calcTickmarkPositions(self):
 
774
        """Calculate a list of tick positions on the axis.  Returns a list of numbers."""
 
775
        self._tickValues = getattr(self,'valueSteps',None)
 
776
        if self._tickValues: return self._tickValues
 
777
        self._tickValues = self._calcTickPositions()
 
778
        self._adjustAxisTicks()
 
779
        return self._tickValues
 
780
 
 
781
    def _calcValueStep(self):
 
782
        '''Calculate _valueStep for the axis or get from valueStep.'''
 
783
        if self.valueStep is None:
 
784
            rawRange = self._valueMax - self._valueMin
 
785
            rawInterval = rawRange / min(float(self.maximumTicks-1),(float(self._length)/self.minimumTickSpacing))
 
786
            self._valueStep = nextRoundNumber(rawInterval)
 
787
        else:
 
788
            self._valueStep = self.valueStep
 
789
 
 
790
    def _allIntTicks(self):
 
791
        for tick in self._tickValues:
 
792
            try:
 
793
                if int(tick)!=tick: return 0
 
794
            except:
 
795
                return 0
 
796
        return 1
 
797
 
 
798
    def makeTickLabels(self):
 
799
        g = Group()
 
800
        if not self.visibleLabels: return g
 
801
 
 
802
        f = self._labelTextFormat       # perhaps someone already set it
 
803
        if f is None:
 
804
            f = self.labelTextFormat or (self._allIntTicks() and '%.0f' or str)
 
805
        elif f is str and self._allIntTicks(): f = '%.0f'
 
806
        post = self.labelTextPostFormat
 
807
        scl = self.labelTextScale
 
808
        pos = [self._x, self._y]
 
809
        d = self._dataIndex
 
810
        labels = self.labels
 
811
 
 
812
        i = 0
 
813
        for tick in self._tickValues:
 
814
            if f and labels[i].visible:
 
815
                v = self.scale(tick)
 
816
                if scl is not None:
 
817
                    t = tick*scl
 
818
                else:
 
819
                    t = tick
 
820
                if type(f) is StringType: txt = f % t
 
821
                elif type(f) in (TupleType,ListType):
 
822
                    #it's a list, use as many items as we get
 
823
                    if i < len(f):
 
824
                        txt = f[i]
 
825
                    else:
 
826
                        txt = ''
 
827
                elif callable(f):
 
828
                    txt = f(t)
 
829
                else:
 
830
                    raise ValueError, 'Invalid labelTextFormat %s' % f
 
831
                if post: txt = post % txt
 
832
                label = labels[i]
 
833
                pos[d] = v
 
834
                apply(label.setOrigin,pos)
 
835
                label.setText(txt)
 
836
                g.add(label)
 
837
            i = i + 1
 
838
 
 
839
        return g
 
840
 
 
841
    def draw(self):
 
842
        g = Group()
 
843
 
 
844
        if not self.visible:
 
845
            return g
 
846
 
 
847
        g.add(self.makeAxis())
 
848
        g.add(self.makeTicks())
 
849
        g.add(self.makeTickLabels())
 
850
 
 
851
        return g
 
852
 
 
853
 
 
854
class XValueAxis(ValueAxis):
 
855
    "X/value axis"
 
856
 
 
857
    _attrMap = AttrMap(BASE=ValueAxis,
 
858
        tickUp = AttrMapValue(isNumber,
 
859
            desc='Tick length up the axis.'),
 
860
        tickDown = AttrMapValue(isNumber,
 
861
            desc='Tick length down the axis.'),
 
862
        joinAxis = AttrMapValue(None,
 
863
            desc='Join both axes if true.'),
 
864
        joinAxisMode = AttrMapValue(OneOf('bottom', 'top', 'value', 'points', None),
 
865
            desc="Mode used for connecting axis ('bottom', 'top', 'value', 'points', None)."),
 
866
        joinAxisPos = AttrMapValue(isNumberOrNone,
 
867
            desc='Position at which to join with other axis.'),
 
868
        )
 
869
 
 
870
    # Indicate the dimension of the data we're interested in.
 
871
    _dataIndex = 0
 
872
 
 
873
    def __init__(self):
 
874
        ValueAxis.__init__(self)
 
875
 
 
876
        self.labels.boxAnchor = 'n'
 
877
        self.labels.dx = 0
 
878
        self.labels.dy = -5
 
879
 
 
880
        self.tickUp = 0
 
881
        self.tickDown = 5
 
882
 
 
883
        self.joinAxis = None
 
884
        self.joinAxisMode = None
 
885
        self.joinAxisPos = None
 
886
 
 
887
 
 
888
    def demo(self):
 
889
        self.setPosition(20, 50, 150)
 
890
        self.configure([(10,20,30,40,50)])
 
891
 
 
892
        d = Drawing(200, 100)
 
893
        d.add(self)
 
894
        return d
 
895
 
 
896
 
 
897
    def joinToAxis(self, yAxis, mode='bottom', pos=None):
 
898
        "Join with y-axis using some mode."
 
899
        _assertYAxis(yAxis)
 
900
        if mode == 'bottom':
 
901
            self._x = yAxis._x * 1.0
 
902
            self._y = yAxis._y * 1.0
 
903
        elif mode == 'top':
 
904
            self._x = yAxis._x * 1.0
 
905
            self._y = (yAxis._y + yAxis._length) * 1.0
 
906
        elif mode == 'value':
 
907
            self._x = yAxis._x * 1.0
 
908
            self._y = yAxis.scale(pos) * 1.0
 
909
        elif mode == 'points':
 
910
            self._x = yAxis._x * 1.0
 
911
            self._y = pos * 1.0
 
912
 
 
913
    def scale(self, value):
 
914
        """Converts a numeric value to a Y position.
 
915
 
 
916
        The chart first configures the axis, then asks it to
 
917
        work out the x value for each point when plotting
 
918
        lines or bars.  You could override this to do
 
919
        logarithmic axes.
 
920
        """
 
921
 
 
922
        msg = "Axis cannot scale numbers before it is configured"
 
923
        assert self._configured, msg
 
924
        if value is None:
 
925
            value = 0
 
926
        return self._x + self._scaleFactor * (value - self._valueMin)
 
927
 
 
928
 
 
929
    def makeAxis(self):
 
930
        g = Group()
 
931
 
 
932
        if not self.visibleAxis:
 
933
            return g
 
934
 
 
935
        ja = self.joinAxis
 
936
        if ja:
 
937
            jam = self.joinAxisMode
 
938
            jap = self.joinAxisPos
 
939
            jta = self.joinToAxis
 
940
            if jam in ('bottom', 'top'):
 
941
                jta(ja, mode=jam)
 
942
            elif jam in ('value', 'points'):
 
943
                jta(ja, mode=jam, pos=jap)
 
944
 
 
945
        axis = Line(self._x, self._y, self._x + self._length, self._y)
 
946
        axis.strokeColor = self.strokeColor
 
947
        axis.strokeWidth = self.strokeWidth
 
948
        axis.strokeDashArray = self.strokeDashArray
 
949
        g.add(axis)
 
950
 
 
951
        return g
 
952
 
 
953
    def makeTicks(self):
 
954
        g = Group()
 
955
        if self.visibleTicks and (self.tickUp or self.tickDown):
 
956
            self._makeLines(g,-self.tickDown,self.tickUp,self.strokeColor,self.strokeWidth,self.strokeDashArray)
 
957
        return g
 
958
 
 
959
class NormalDateXValueAxis(XValueAxis):
 
960
    """An X axis applying additional rules.
 
961
 
 
962
    Depending on the data and some built-in rules, the axis
 
963
    displays normalDate values as nicely formatted dates.
 
964
 
 
965
    The client chart should have NormalDate X values.
 
966
    """
 
967
 
 
968
    _attrMap = AttrMap(BASE = XValueAxis,
 
969
        bottomAxisLabelSlack = AttrMapValue(isNumber, desc="Fractional amount used to adjust label spacing"),
 
970
        niceMonth = AttrMapValue(isBoolean, desc="Flag for displaying months 'nicely'."),
 
971
        forceEndDate = AttrMapValue(isBoolean, desc='Flag for enforced displaying of last date value.'),
 
972
        forceFirstDate = AttrMapValue(isBoolean, desc='Flag for enforced displaying of first date value.'),
 
973
        xLabelFormat = AttrMapValue(None, desc="Label format string (e.g. '{mm}/{yy}') or function."),
 
974
        dayOfWeekName = AttrMapValue(SequenceOf(isString,emptyOK=0,lo=7,hi=7), desc='Weekday names.'),
 
975
        monthName = AttrMapValue(SequenceOf(isString,emptyOK=0,lo=12,hi=12), desc='Month names.'),
 
976
        dailyFreq = AttrMapValue(isBoolean, desc='True if we are to assume daily data to be ticked at end of month.'),
 
977
        )
 
978
 
 
979
    _valueClass = normalDate.ND
 
980
 
 
981
    def __init__(self, **kw):
 
982
        apply(XValueAxis.__init__, (self,))
 
983
 
 
984
        # some global variables still used...
 
985
        self.bottomAxisLabelSlack = 0.1
 
986
        self.niceMonth = 1
 
987
        self.forceEndDate = 0
 
988
        self.forceFirstDate = 0
 
989
        self.dailyFreq = 0
 
990
        self.xLabelFormat = "{mm}/{yy}"
 
991
        self.dayOfWeekName = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
 
992
        self.monthName = ['January', 'February', 'March', 'April', 'May', 'June', 'July',
 
993
                            'August', 'September', 'October', 'November', 'December']
 
994
        self.valueSteps = None
 
995
 
 
996
    def _scalar2ND(self, x):
 
997
        "Convert a scalar to a NormalDate value."
 
998
        d = self._valueClass()
 
999
        d.normalize(x)
 
1000
        return d
 
1001
 
 
1002
    def _dateFormatter(self, v):
 
1003
        "Create a formatted label for some value."
 
1004
        if not isinstance(v,normalDate.NormalDate):
 
1005
            v = self._scalar2ND(v)
 
1006
        d, m = normalDate._dayOfWeekName, normalDate._monthName
 
1007
        try:
 
1008
            normalDate._dayOfWeekName, normalDate._monthName = self.dayOfWeekName, self.monthName
 
1009
            return v.formatMS(self.xLabelFormat)
 
1010
        finally:
 
1011
            normalDate._dayOfWeekName, normalDate._monthName = d, m
 
1012
 
 
1013
    def _xAxisTicker(self, xVals):
 
1014
        """Complex stuff...
 
1015
 
 
1016
        Needs explanation...
 
1017
        """
 
1018
        axisLength = self._length
 
1019
        formatter = self._dateFormatter
 
1020
        labels = self.labels
 
1021
        fontName, fontSize, leading = labels.fontName, labels.fontSize, labels.leading
 
1022
        textAnchor, boxAnchor, angle = labels.textAnchor, labels.boxAnchor, labels.angle
 
1023
        RBL = _textBoxLimits(string.split(formatter(xVals[0]),'\n'),fontName,
 
1024
                    fontSize,leading or 1.2*fontSize,textAnchor,boxAnchor)
 
1025
        RBL = _rotatedBoxLimits(RBL[0],RBL[1],RBL[2],RBL[3], angle)
 
1026
        xLabelW = RBL[1]-RBL[0]
 
1027
        xLabelH = RBL[3]-RBL[2]
 
1028
        w = max(xLabelW,labels.width,self.minimumTickSpacing)
 
1029
 
 
1030
        W = w+w*self.bottomAxisLabelSlack
 
1031
        n = len(xVals)
 
1032
        ticks = []
 
1033
        labels = []
 
1034
        maximumTicks = self.maximumTicks
 
1035
 
 
1036
        def addTick(i, xVals=xVals, formatter=formatter, ticks=ticks, labels=labels):
 
1037
            ticks.insert(0,xVals[i])
 
1038
            labels.insert(0,formatter(xVals[i]))
 
1039
 
 
1040
        for d in (1,2,3,6,12,24,60,120):
 
1041
            k = n/d
 
1042
            if k<=maximumTicks and k*W <= axisLength:
 
1043
                i = n-1
 
1044
                if self.niceMonth:
 
1045
                    j = xVals[-1].month() % (d<=12 and d or 12)
 
1046
                    if j:
 
1047
                        if self.forceEndDate: addTick(i)
 
1048
                        i = i - j
 
1049
 
 
1050
                #weird first date ie not at end of month
 
1051
                try:
 
1052
                    wfd = xVals[0].month() == xVals[1].month()
 
1053
                except:
 
1054
                    wfd = 0
 
1055
 
 
1056
                while i>=wfd:
 
1057
                    addTick(i)
 
1058
                    i = i - d
 
1059
 
 
1060
                if self.forceFirstDate and ticks[0] != xVals[0]:
 
1061
                    addTick(0)
 
1062
                    if (axisLength/(ticks[-1]-ticks[0]))*(ticks[1]-ticks[0])<=w:
 
1063
                        del ticks[1], labels[1]
 
1064
                if self.forceEndDate and self.niceMonth and j:
 
1065
                    if (axisLength/(ticks[-1]-ticks[0]))*(ticks[-1]-ticks[-2])<=w:
 
1066
                        del ticks[-2], labels[-2]
 
1067
                try:
 
1068
                    if labels[0] and labels[0]==labels[1]:
 
1069
                        del ticks[1], labels[1]
 
1070
                except IndexError:
 
1071
                    pass
 
1072
 
 
1073
                return ticks, labels
 
1074
 
 
1075
    def _convertXV(self,data):
 
1076
        '''Convert all XValues to a standard normalDate type'''
 
1077
 
 
1078
        VC = self._valueClass
 
1079
        for D in data:
 
1080
            for i in xrange(len(D)):
 
1081
                x, y = D[i]
 
1082
                if not isinstance(x,VC):
 
1083
                    D[i] = (VC(x),y)
 
1084
 
 
1085
    def _getStepsAndLabels(self,xVals):
 
1086
        if self.dailyFreq:
 
1087
            xEOM = []
 
1088
            pm = 0
 
1089
            px = xVals[0]
 
1090
            for x in xVals:
 
1091
                m = x.month()
 
1092
                if pm!=m:
 
1093
                    if pm: xEOM.append(px)
 
1094
                    pm = m
 
1095
                px = x
 
1096
            px = xVals[-1]
 
1097
            if xEOM[-1]!=x: xEOM.append(px)
 
1098
            steps, labels = self._xAxisTicker(xEOM)
 
1099
        else:
 
1100
            steps, labels = self._xAxisTicker(xVals)
 
1101
        return steps, labels
 
1102
 
 
1103
    def configure(self, data):
 
1104
        self._convertXV(data)
 
1105
        from reportlab.lib.set_ops import union
 
1106
        xVals = reduce(union,map(lambda x: map(lambda dv: dv[0],x),data),[])
 
1107
        xVals.sort()
 
1108
        steps,labels = self._getStepsAndLabels(xVals)
 
1109
        valueMin, valueMax = self.valueMin, self.valueMax
 
1110
        if valueMin is None: valueMin = xVals[0]
 
1111
        if valueMax is None: valueMax = xVals[-1]
 
1112
        self._valueMin, self._valueMax = valueMin, valueMax
 
1113
        self._tickValues = steps
 
1114
        self._labelTextFormat = labels
 
1115
 
 
1116
        self._scaleFactor = self._length / float(valueMax - valueMin)
 
1117
        self._tickValues = steps
 
1118
        self._configured = 1
 
1119
 
 
1120
class YValueAxis(ValueAxis):
 
1121
    "Y/value axis"
 
1122
 
 
1123
    _attrMap = AttrMap(BASE=ValueAxis,
 
1124
        tickLeft = AttrMapValue(isNumber,
 
1125
            desc='Tick length left of the axis.'),
 
1126
        tickRight = AttrMapValue(isNumber,
 
1127
            desc='Tick length right of the axis.'),
 
1128
        joinAxis = AttrMapValue(None,
 
1129
            desc='Join both axes if true.'),
 
1130
        joinAxisMode = AttrMapValue(OneOf(('left', 'right', 'value', 'points', None)),
 
1131
            desc="Mode used for connecting axis ('left', 'right', 'value', 'points', None)."),
 
1132
        joinAxisPos = AttrMapValue(isNumberOrNone,
 
1133
            desc='Position at which to join with other axis.'),
 
1134
        )
 
1135
 
 
1136
    # Indicate the dimension of the data we're interested in.
 
1137
    _dataIndex = 1
 
1138
 
 
1139
    def __init__(self):
 
1140
        ValueAxis.__init__(self)
 
1141
 
 
1142
        self.labels.boxAnchor = 'e'
 
1143
        self.labels.dx = -5
 
1144
        self.labels.dy = 0
 
1145
 
 
1146
        self.tickRight = 0
 
1147
        self.tickLeft = 5
 
1148
 
 
1149
        self.joinAxis = None
 
1150
        self.joinAxisMode = None
 
1151
        self.joinAxisPos = None
 
1152
 
 
1153
 
 
1154
    def demo(self):
 
1155
        data = [(10, 20, 30, 42)]
 
1156
        self.setPosition(100, 10, 80)
 
1157
        self.configure(data)
 
1158
 
 
1159
        drawing = Drawing(200, 100)
 
1160
        drawing.add(self)
 
1161
        return drawing
 
1162
 
 
1163
 
 
1164
 
 
1165
    def joinToAxis(self, xAxis, mode='left', pos=None):
 
1166
        "Join with x-axis using some mode."
 
1167
        _assertXAxis(xAxis)
 
1168
        if mode == 'left':
 
1169
            self._x = xAxis._x * 1.0
 
1170
            self._y = xAxis._y * 1.0
 
1171
        elif mode == 'right':
 
1172
            self._x = (xAxis._x + xAxis._length) * 1.0
 
1173
            self._y = xAxis._y * 1.0
 
1174
        elif mode == 'value':
 
1175
            self._x = xAxis.scale(pos) * 1.0
 
1176
            self._y = xAxis._y * 1.0
 
1177
        elif mode == 'points':
 
1178
            self._x = pos * 1.0
 
1179
            self._y = xAxis._y * 1.0
 
1180
 
 
1181
    def scale(self, value):
 
1182
        """Converts a numeric value to a Y position.
 
1183
 
 
1184
        The chart first configures the axis, then asks it to
 
1185
        work out the x value for each point when plotting
 
1186
        lines or bars.  You could override this to do
 
1187
        logarithmic axes.
 
1188
        """
 
1189
 
 
1190
        msg = "Axis cannot scale numbers before it is configured"
 
1191
        assert self._configured, msg
 
1192
 
 
1193
        if value is None:
 
1194
            value = 0
 
1195
        return self._y + self._scaleFactor * (value - self._valueMin)
 
1196
 
 
1197
 
 
1198
    def makeAxis(self):
 
1199
        g = Group()
 
1200
 
 
1201
        if not self.visibleAxis:
 
1202
            return g
 
1203
 
 
1204
        ja = self.joinAxis
 
1205
        if ja:
 
1206
            jam = self.joinAxisMode
 
1207
            jap = self.joinAxisPos
 
1208
            jta = self.joinToAxis
 
1209
            if jam in ('left', 'right'):
 
1210
                jta(ja, mode=jam)
 
1211
            elif jam in ('value', 'points'):
 
1212
                jta(ja, mode=jam, pos=jap)
 
1213
 
 
1214
        axis = Line(self._x, self._y, self._x, self._y + self._length)
 
1215
        axis.strokeColor = self.strokeColor
 
1216
        axis.strokeWidth = self.strokeWidth
 
1217
        axis.strokeDashArray = self.strokeDashArray
 
1218
        g.add(axis)
 
1219
 
 
1220
        return g
 
1221
 
 
1222
    def makeTicks(self):
 
1223
        g = Group()
 
1224
        if self.visibleTicks and (self.tickLeft or self.tickRight):
 
1225
            self._makeLines(g,-self.tickLeft,self.tickRight,self.strokeColor,self.strokeWidth,self.strokeDashArray)
 
1226
        return g
 
1227
 
 
1228
class AdjYValueAxis(YValueAxis):
 
1229
    """A Y-axis applying additional rules.
 
1230
 
 
1231
    Depending on the data and some built-in rules, the axis
 
1232
    may choose to adjust its range and origin.
 
1233
    """
 
1234
    _attrMap = AttrMap(BASE = YValueAxis,
 
1235
        requiredRange = AttrMapValue(isNumberOrNone, desc='Minimum required value range.'),
 
1236
        leftAxisPercent = AttrMapValue(isBoolean, desc='When true add percent sign to label values.'),
 
1237
        leftAxisOrigShiftIPC = AttrMapValue(isNumber, desc='Lowest label shift interval ratio.'),
 
1238
        leftAxisOrigShiftMin = AttrMapValue(isNumber, desc='Minimum amount to shift.'),
 
1239
        leftAxisSkipLL0 = AttrMapValue(EitherOr((isBoolean,isListOfNumbers)), desc='Skip/Keep lowest tick label when true/false.\nOr skiplist')
 
1240
        )
 
1241
 
 
1242
    def __init__(self):
 
1243
        apply(YValueAxis.__init__, (self,))
 
1244
        self.requiredRange = 30
 
1245
        self.leftAxisPercent = 1
 
1246
        self.leftAxisOrigShiftIPC = 0.15
 
1247
        self.leftAxisOrigShiftMin = 12
 
1248
        self.leftAxisSkipLL0 = 0
 
1249
        self.valueSteps = None
 
1250
 
 
1251
    def _rangeAdjust(self):
 
1252
        "Adjusts the value range of the axis."
 
1253
 
 
1254
        from reportlab.graphics.charts.utils import find_good_grid, ticks
 
1255
        y_min, y_max = self._valueMin, self._valueMax
 
1256
        m = self.maximumTicks
 
1257
        n = filter(lambda x,m=m: x<=m,[4,5,6,7,8,9])
 
1258
        if not n: n = [m]
 
1259
 
 
1260
        valueStep, requiredRange = self.valueStep, self.requiredRange
 
1261
        if requiredRange and y_max - y_min < requiredRange:
 
1262
            y1, y2 = find_good_grid(y_min, y_max,n=n,grid=valueStep)[:2]
 
1263
            if y2 - y1 < requiredRange:
 
1264
                ym = (y1+y2)*0.5
 
1265
                y1 = min(ym-requiredRange*0.5,y_min)
 
1266
                y2 = max(ym+requiredRange*0.5,y_max)
 
1267
                if y_min>=100 and y1<100:
 
1268
                    y2 = y2 + 100 - y1
 
1269
                    y1 = 100
 
1270
                elif y_min>=0 and y1<0:
 
1271
                    y2 = y2 - y1
 
1272
                    y1 = 0
 
1273
            self._valueMin, self._valueMax = y1, y2
 
1274
 
 
1275
        T, L = ticks(self._valueMin, self._valueMax, split=1, n=n, percent=self.leftAxisPercent,grid=valueStep)
 
1276
        abf = self.avoidBoundFrac
 
1277
        if abf:
 
1278
            i1 = (T[1]-T[0])
 
1279
            if type(abf) not in (TupleType,ListType):
 
1280
                i0 = i1 = i1*abf
 
1281
            else:
 
1282
                i0 = i1*abf[0]
 
1283
                i1 = i1*abf[1]
 
1284
            _n = getattr(self,'_cValueMin',T[0])
 
1285
            _x = getattr(self,'_cValueMax',T[-1])
 
1286
            if _n - T[0] < i0: self._valueMin = self._valueMin - i0
 
1287
            if T[-1]-_x < i1: self._valueMax = self._valueMax + i1
 
1288
            T, L = ticks(self._valueMin, self._valueMax, split=1, n=n, percent=self.leftAxisPercent,grid=valueStep)
 
1289
 
 
1290
        self._valueMin = T[0]
 
1291
        self._valueMax = T[-1]
 
1292
        self._tickValues = self.valueSteps = T
 
1293
        if self.labelTextFormat is None:
 
1294
            self._labelTextFormat = L
 
1295
        else:
 
1296
            self._labelTextFormat = self.labelTextFormat
 
1297
 
 
1298
        if abs(self._valueMin-100)<1e-6:
 
1299
            self._calcValueStep()
 
1300
            vMax, vMin = self._valueMax, self._valueMin
 
1301
            m = max(self.leftAxisOrigShiftIPC*self._valueStep,
 
1302
                    (vMax-vMin)*self.leftAxisOrigShiftMin/self._length)
 
1303
            self._valueMin = self._valueMin - m
 
1304
 
 
1305
        if self.leftAxisSkipLL0:
 
1306
            if type(self.leftAxisSkipLL0) in (ListType,TupleType):
 
1307
                for x in self.leftAxisSkipLL0:
 
1308
                    try:
 
1309
                        L[x] = ''
 
1310
                    except IndexError:
 
1311
                        pass
 
1312
            L[0] = ''
 
1313
 
 
1314
# Sample functions.
 
1315
def sample0a():
 
1316
    "Sample drawing with one xcat axis and two buckets."
 
1317
 
 
1318
    drawing = Drawing(400, 200)
 
1319
 
 
1320
    data = [(10, 20)]
 
1321
 
 
1322
    xAxis = XCategoryAxis()
 
1323
    xAxis.setPosition(75, 75, 300)
 
1324
    xAxis.configure(data)
 
1325
    xAxis.categoryNames = ['Ying', 'Yang']
 
1326
    xAxis.labels.boxAnchor = 'n'
 
1327
 
 
1328
    drawing.add(xAxis)
 
1329
 
 
1330
    return drawing
 
1331
 
 
1332
 
 
1333
def sample0b():
 
1334
    "Sample drawing with one xcat axis and one bucket only."
 
1335
 
 
1336
    drawing = Drawing(400, 200)
 
1337
 
 
1338
    data = [(10,)]
 
1339
 
 
1340
    xAxis = XCategoryAxis()
 
1341
    xAxis.setPosition(75, 75, 300)
 
1342
    xAxis.configure(data)
 
1343
    xAxis.categoryNames = ['Ying']
 
1344
    xAxis.labels.boxAnchor = 'n'
 
1345
 
 
1346
    drawing.add(xAxis)
 
1347
 
 
1348
    return drawing
 
1349
 
 
1350
 
 
1351
def sample1():
 
1352
    "Sample drawing containing two unconnected axes."
 
1353
 
 
1354
    drawing = Drawing(400, 200)
 
1355
 
 
1356
    data = [(10, 20, 30, 42)]
 
1357
 
 
1358
    xAxis = XCategoryAxis()
 
1359
    xAxis.setPosition(75, 75, 300)
 
1360
    xAxis.configure(data)
 
1361
    xAxis.categoryNames = ['Beer','Wine','Meat','Cannelloni']
 
1362
    xAxis.labels.boxAnchor = 'n'
 
1363
    xAxis.labels[3].dy = -15
 
1364
    xAxis.labels[3].angle = 30
 
1365
    xAxis.labels[3].fontName = 'Times-Bold'
 
1366
 
 
1367
    yAxis = YValueAxis()
 
1368
    yAxis.setPosition(50, 50, 125)
 
1369
    yAxis.configure(data)
 
1370
    drawing.add(xAxis)
 
1371
    drawing.add(yAxis)
 
1372
 
 
1373
    return drawing
 
1374
 
 
1375
 
 
1376
##def sample2a():
 
1377
##  "Make sample drawing with two axes, x connected at top of y."
 
1378
##
 
1379
##    drawing = Drawing(400, 200)
 
1380
##
 
1381
##    data = [(10, 20, 30, 42)]
 
1382
##
 
1383
##    yAxis = YValueAxis()
 
1384
##    yAxis.setPosition(50, 50, 125)
 
1385
##    yAxis.configure(data)
 
1386
##
 
1387
##    xAxis = XCategoryAxis()
 
1388
##    xAxis._length = 300
 
1389
##    xAxis.configure(data)
 
1390
##    xAxis.joinToAxis(yAxis, mode='top')
 
1391
##    xAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni']
 
1392
##    xAxis.labels.boxAnchor = 'n'
 
1393
##
 
1394
##    drawing.add(xAxis)
 
1395
##    drawing.add(yAxis)
 
1396
##
 
1397
##    return drawing
 
1398
##
 
1399
##
 
1400
##def sample2b():
 
1401
##    "Make two axes, x connected at bottom of y."
 
1402
##
 
1403
##    drawing = Drawing(400, 200)
 
1404
##
 
1405
##    data = [(10, 20, 30, 42)]
 
1406
##
 
1407
##    yAxis = YValueAxis()
 
1408
##    yAxis.setPosition(50, 50, 125)
 
1409
##    yAxis.configure(data)
 
1410
##
 
1411
##    xAxis = XCategoryAxis()
 
1412
##    xAxis._length = 300
 
1413
##    xAxis.configure(data)
 
1414
##    xAxis.joinToAxis(yAxis, mode='bottom')
 
1415
##    xAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni']
 
1416
##    xAxis.labels.boxAnchor = 'n'
 
1417
##
 
1418
##    drawing.add(xAxis)
 
1419
##    drawing.add(yAxis)
 
1420
##
 
1421
##    return drawing
 
1422
##
 
1423
##
 
1424
##def sample2c():
 
1425
##    "Make two axes, x connected at fixed value (in points) of y."
 
1426
##
 
1427
##    drawing = Drawing(400, 200)
 
1428
##
 
1429
##    data = [(10, 20, 30, 42)]
 
1430
##
 
1431
##    yAxis = YValueAxis()
 
1432
##    yAxis.setPosition(50, 50, 125)
 
1433
##    yAxis.configure(data)
 
1434
##
 
1435
##    xAxis = XCategoryAxis()
 
1436
##    xAxis._length = 300
 
1437
##    xAxis.configure(data)
 
1438
##    xAxis.joinToAxis(yAxis, mode='points', pos=100)
 
1439
##    xAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni']
 
1440
##    xAxis.labels.boxAnchor = 'n'
 
1441
##
 
1442
##    drawing.add(xAxis)
 
1443
##    drawing.add(yAxis)
 
1444
##
 
1445
##    return drawing
 
1446
##
 
1447
##
 
1448
##def sample2d():
 
1449
##    "Make two axes, x connected at fixed value (of y-axes) of y."
 
1450
##
 
1451
##    drawing = Drawing(400, 200)
 
1452
##
 
1453
##    data = [(10, 20, 30, 42)]
 
1454
##
 
1455
##    yAxis = YValueAxis()
 
1456
##    yAxis.setPosition(50, 50, 125)
 
1457
##    yAxis.configure(data)
 
1458
##
 
1459
##    xAxis = XCategoryAxis()
 
1460
##    xAxis._length = 300
 
1461
##    xAxis.configure(data)
 
1462
##    xAxis.joinToAxis(yAxis, mode='value', pos=20)
 
1463
##    xAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni']
 
1464
##    xAxis.labels.boxAnchor = 'n'
 
1465
##
 
1466
##    drawing.add(xAxis)
 
1467
##    drawing.add(yAxis)
 
1468
##
 
1469
##    return drawing
 
1470
##
 
1471
##
 
1472
##def sample3a():
 
1473
##    "Make sample drawing with two axes, y connected at left of x."
 
1474
##
 
1475
##    drawing = Drawing(400, 200)
 
1476
##
 
1477
##    data = [(10, 20, 30, 42)]
 
1478
##
 
1479
##    xAxis = XCategoryAxis()
 
1480
##    xAxis._length = 300
 
1481
##    xAxis.configure(data)
 
1482
##    xAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni']
 
1483
##    xAxis.labels.boxAnchor = 'n'
 
1484
##
 
1485
##    yAxis = YValueAxis()
 
1486
##    yAxis.setPosition(50, 50, 125)
 
1487
##    yAxis.configure(data)
 
1488
##    yAxis.joinToAxis(xAxis, mode='left')
 
1489
##
 
1490
##    drawing.add(xAxis)
 
1491
##    drawing.add(yAxis)
 
1492
##
 
1493
##    return drawing
 
1494
##
 
1495
##
 
1496
##def sample3b():
 
1497
##    "Make sample drawing with two axes, y connected at right of x."
 
1498
##
 
1499
##    drawing = Drawing(400, 200)
 
1500
##
 
1501
##    data = [(10, 20, 30, 42)]
 
1502
##
 
1503
##    xAxis = XCategoryAxis()
 
1504
##    xAxis._length = 300
 
1505
##    xAxis.configure(data)
 
1506
##    xAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni']
 
1507
##    xAxis.labels.boxAnchor = 'n'
 
1508
##
 
1509
##    yAxis = YValueAxis()
 
1510
##    yAxis.setPosition(50, 50, 125)
 
1511
##    yAxis.configure(data)
 
1512
##    yAxis.joinToAxis(xAxis, mode='right')
 
1513
##
 
1514
##    drawing.add(xAxis)
 
1515
##    drawing.add(yAxis)
 
1516
##
 
1517
##    return drawing
 
1518
##
 
1519
##
 
1520
##def sample3c():
 
1521
##    "Make two axes, y connected at fixed value (in points) of x."
 
1522
##
 
1523
##    drawing = Drawing(400, 200)
 
1524
##
 
1525
##    data = [(10, 20, 30, 42)]
 
1526
##
 
1527
##    yAxis = YValueAxis()
 
1528
##    yAxis.setPosition(50, 50, 125)
 
1529
##    yAxis.configure(data)
 
1530
##
 
1531
##    xAxis = XValueAxis()
 
1532
##    xAxis._length = 300
 
1533
##    xAxis.configure(data)
 
1534
##    xAxis.joinToAxis(yAxis, mode='points', pos=100)
 
1535
##
 
1536
##    drawing.add(xAxis)
 
1537
##    drawing.add(yAxis)
 
1538
##
 
1539
##    return drawing
 
1540
 
 
1541
 
 
1542
def sample4a():
 
1543
    "Sample drawing, xvalue/yvalue axes, y connected at 100 pts to x."
 
1544
 
 
1545
    drawing = Drawing(400, 200)
 
1546
 
 
1547
    data = [(10, 20, 30, 42)]
 
1548
 
 
1549
    yAxis = YValueAxis()
 
1550
    yAxis.setPosition(50, 50, 125)
 
1551
    yAxis.configure(data)
 
1552
 
 
1553
    xAxis = XValueAxis()
 
1554
    xAxis._length = 300
 
1555
    xAxis.joinAxis = yAxis
 
1556
    xAxis.joinAxisMode = 'points'
 
1557
    xAxis.joinAxisPos = 100
 
1558
    xAxis.configure(data)
 
1559
 
 
1560
    drawing.add(xAxis)
 
1561
    drawing.add(yAxis)
 
1562
 
 
1563
    return drawing
 
1564
 
 
1565
 
 
1566
def sample4b():
 
1567
    "Sample drawing, xvalue/yvalue axes, y connected at value 35 of x."
 
1568
 
 
1569
    drawing = Drawing(400, 200)
 
1570
 
 
1571
    data = [(10, 20, 30, 42)]
 
1572
 
 
1573
    yAxis = YValueAxis()
 
1574
    yAxis.setPosition(50, 50, 125)
 
1575
    yAxis.configure(data)
 
1576
 
 
1577
    xAxis = XValueAxis()
 
1578
    xAxis._length = 300
 
1579
    xAxis.joinAxis = yAxis
 
1580
    xAxis.joinAxisMode = 'value'
 
1581
    xAxis.joinAxisPos = 35
 
1582
    xAxis.configure(data)
 
1583
 
 
1584
    drawing.add(xAxis)
 
1585
    drawing.add(yAxis)
 
1586
 
 
1587
    return drawing
 
1588
 
 
1589
 
 
1590
def sample4c():
 
1591
    "Sample drawing, xvalue/yvalue axes, y connected to bottom of x."
 
1592
 
 
1593
    drawing = Drawing(400, 200)
 
1594
 
 
1595
    data = [(10, 20, 30, 42)]
 
1596
 
 
1597
    yAxis = YValueAxis()
 
1598
    yAxis.setPosition(50, 50, 125)
 
1599
    yAxis.configure(data)
 
1600
 
 
1601
    xAxis = XValueAxis()
 
1602
    xAxis._length = 300
 
1603
    xAxis.joinAxis = yAxis
 
1604
    xAxis.joinAxisMode = 'bottom'
 
1605
    xAxis.configure(data)
 
1606
 
 
1607
    drawing.add(xAxis)
 
1608
    drawing.add(yAxis)
 
1609
 
 
1610
    return drawing
 
1611
 
 
1612
 
 
1613
def sample4c1():
 
1614
    "xvalue/yvalue axes, without drawing axis lines/ticks."
 
1615
 
 
1616
    drawing = Drawing(400, 200)
 
1617
 
 
1618
    data = [(10, 20, 30, 42)]
 
1619
 
 
1620
    yAxis = YValueAxis()
 
1621
    yAxis.setPosition(50, 50, 125)
 
1622
    yAxis.configure(data)
 
1623
    yAxis.visibleAxis = 0
 
1624
    yAxis.visibleTicks = 0
 
1625
 
 
1626
    xAxis = XValueAxis()
 
1627
    xAxis._length = 300
 
1628
    xAxis.joinAxis = yAxis
 
1629
    xAxis.joinAxisMode = 'bottom'
 
1630
    xAxis.configure(data)
 
1631
    xAxis.visibleAxis = 0
 
1632
    xAxis.visibleTicks = 0
 
1633
 
 
1634
    drawing.add(xAxis)
 
1635
    drawing.add(yAxis)
 
1636
 
 
1637
    return drawing
 
1638
 
 
1639
 
 
1640
def sample4d():
 
1641
    "Sample drawing, xvalue/yvalue axes, y connected to top of x."
 
1642
 
 
1643
    drawing = Drawing(400, 200)
 
1644
 
 
1645
    data = [(10, 20, 30, 42)]
 
1646
 
 
1647
    yAxis = YValueAxis()
 
1648
    yAxis.setPosition(50, 50, 125)
 
1649
    yAxis.configure(data)
 
1650
 
 
1651
    xAxis = XValueAxis()
 
1652
    xAxis._length = 300
 
1653
    xAxis.joinAxis = yAxis
 
1654
    xAxis.joinAxisMode = 'top'
 
1655
    xAxis.configure(data)
 
1656
 
 
1657
    drawing.add(xAxis)
 
1658
    drawing.add(yAxis)
 
1659
 
 
1660
    return drawing
 
1661
 
 
1662
 
 
1663
def sample5a():
 
1664
    "Sample drawing, xvalue/yvalue axes, y connected at 100 pts to x."
 
1665
 
 
1666
    drawing = Drawing(400, 200)
 
1667
 
 
1668
    data = [(10, 20, 30, 42)]
 
1669
 
 
1670
    xAxis = XValueAxis()
 
1671
    xAxis.setPosition(50, 50, 300)
 
1672
    xAxis.configure(data)
 
1673
 
 
1674
    yAxis = YValueAxis()
 
1675
    yAxis.setPosition(50, 50, 125)
 
1676
    yAxis.joinAxis = xAxis
 
1677
    yAxis.joinAxisMode = 'points'
 
1678
    yAxis.joinAxisPos = 100
 
1679
    yAxis.configure(data)
 
1680
 
 
1681
    drawing.add(xAxis)
 
1682
    drawing.add(yAxis)
 
1683
 
 
1684
    return drawing
 
1685
 
 
1686
 
 
1687
def sample5b():
 
1688
    "Sample drawing, xvalue/yvalue axes, y connected at value 35 of x."
 
1689
 
 
1690
    drawing = Drawing(400, 200)
 
1691
 
 
1692
    data = [(10, 20, 30, 42)]
 
1693
 
 
1694
    xAxis = XValueAxis()
 
1695
    xAxis.setPosition(50, 50, 300)
 
1696
    xAxis.configure(data)
 
1697
 
 
1698
    yAxis = YValueAxis()
 
1699
    yAxis.setPosition(50, 50, 125)
 
1700
    yAxis.joinAxis = xAxis
 
1701
    yAxis.joinAxisMode = 'value'
 
1702
    yAxis.joinAxisPos = 35
 
1703
    yAxis.configure(data)
 
1704
 
 
1705
    drawing.add(xAxis)
 
1706
    drawing.add(yAxis)
 
1707
 
 
1708
    return drawing
 
1709
 
 
1710
 
 
1711
def sample5c():
 
1712
    "Sample drawing, xvalue/yvalue axes, y connected at right of x."
 
1713
 
 
1714
    drawing = Drawing(400, 200)
 
1715
 
 
1716
    data = [(10, 20, 30, 42)]
 
1717
 
 
1718
    xAxis = XValueAxis()
 
1719
    xAxis.setPosition(50, 50, 300)
 
1720
    xAxis.configure(data)
 
1721
 
 
1722
    yAxis = YValueAxis()
 
1723
    yAxis.setPosition(50, 50, 125)
 
1724
    yAxis.joinAxis = xAxis
 
1725
    yAxis.joinAxisMode = 'right'
 
1726
    yAxis.configure(data)
 
1727
 
 
1728
    drawing.add(xAxis)
 
1729
    drawing.add(yAxis)
 
1730
 
 
1731
    return drawing
 
1732
 
 
1733
 
 
1734
def sample5d():
 
1735
    "Sample drawing, xvalue/yvalue axes, y connected at left of x."
 
1736
 
 
1737
    drawing = Drawing(400, 200)
 
1738
 
 
1739
    data = [(10, 20, 30, 42)]
 
1740
 
 
1741
    xAxis = XValueAxis()
 
1742
    xAxis.setPosition(50, 50, 300)
 
1743
    xAxis.configure(data)
 
1744
 
 
1745
    yAxis = YValueAxis()
 
1746
    yAxis.setPosition(50, 50, 125)
 
1747
    yAxis.joinAxis = xAxis
 
1748
    yAxis.joinAxisMode = 'left'
 
1749
    yAxis.configure(data)
 
1750
 
 
1751
    drawing.add(xAxis)
 
1752
    drawing.add(yAxis)
 
1753
 
 
1754
    return drawing
 
1755
 
 
1756
 
 
1757
def sample6a():
 
1758
    "Sample drawing, xcat/yvalue axes, x connected at top of y."
 
1759
 
 
1760
    drawing = Drawing(400, 200)
 
1761
 
 
1762
    data = [(10, 20, 30, 42)]
 
1763
 
 
1764
    yAxis = YValueAxis()
 
1765
    yAxis.setPosition(50, 50, 125)
 
1766
    yAxis.configure(data)
 
1767
 
 
1768
    xAxis = XCategoryAxis()
 
1769
    xAxis._length = 300
 
1770
    xAxis.configure(data)
 
1771
    xAxis.joinAxis = yAxis
 
1772
    xAxis.joinAxisMode = 'top'
 
1773
    xAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni']
 
1774
    xAxis.labels.boxAnchor = 'n'
 
1775
 
 
1776
    drawing.add(xAxis)
 
1777
    drawing.add(yAxis)
 
1778
 
 
1779
    return drawing
 
1780
 
 
1781
 
 
1782
def sample6b():
 
1783
    "Sample drawing, xcat/yvalue axes, x connected at bottom of y."
 
1784
 
 
1785
    drawing = Drawing(400, 200)
 
1786
 
 
1787
    data = [(10, 20, 30, 42)]
 
1788
 
 
1789
    yAxis = YValueAxis()
 
1790
    yAxis.setPosition(50, 50, 125)
 
1791
    yAxis.configure(data)
 
1792
 
 
1793
    xAxis = XCategoryAxis()
 
1794
    xAxis._length = 300
 
1795
    xAxis.configure(data)
 
1796
    xAxis.joinAxis = yAxis
 
1797
    xAxis.joinAxisMode = 'bottom'
 
1798
    xAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni']
 
1799
    xAxis.labels.boxAnchor = 'n'
 
1800
 
 
1801
    drawing.add(xAxis)
 
1802
    drawing.add(yAxis)
 
1803
 
 
1804
    return drawing
 
1805
 
 
1806
 
 
1807
def sample6c():
 
1808
    "Sample drawing, xcat/yvalue axes, x connected at 100 pts to y."
 
1809
 
 
1810
    drawing = Drawing(400, 200)
 
1811
 
 
1812
    data = [(10, 20, 30, 42)]
 
1813
 
 
1814
    yAxis = YValueAxis()
 
1815
    yAxis.setPosition(50, 50, 125)
 
1816
    yAxis.configure(data)
 
1817
 
 
1818
    xAxis = XCategoryAxis()
 
1819
    xAxis._length = 300
 
1820
    xAxis.configure(data)
 
1821
    xAxis.joinAxis = yAxis
 
1822
    xAxis.joinAxisMode = 'points'
 
1823
    xAxis.joinAxisPos = 100
 
1824
    xAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni']
 
1825
    xAxis.labels.boxAnchor = 'n'
 
1826
 
 
1827
    drawing.add(xAxis)
 
1828
    drawing.add(yAxis)
 
1829
 
 
1830
    return drawing
 
1831
 
 
1832
 
 
1833
def sample6d():
 
1834
    "Sample drawing, xcat/yvalue axes, x connected at value 20 of y."
 
1835
 
 
1836
    drawing = Drawing(400, 200)
 
1837
 
 
1838
    data = [(10, 20, 30, 42)]
 
1839
 
 
1840
    yAxis = YValueAxis()
 
1841
    yAxis.setPosition(50, 50, 125)
 
1842
    yAxis.configure(data)
 
1843
 
 
1844
    xAxis = XCategoryAxis()
 
1845
    xAxis._length = 300
 
1846
    xAxis.configure(data)
 
1847
    xAxis.joinAxis = yAxis
 
1848
    xAxis.joinAxisMode = 'value'
 
1849
    xAxis.joinAxisPos = 20
 
1850
    xAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni']
 
1851
    xAxis.labels.boxAnchor = 'n'
 
1852
 
 
1853
    drawing.add(xAxis)
 
1854
    drawing.add(yAxis)
 
1855
 
 
1856
    return drawing
 
1857
 
 
1858
 
 
1859
def sample7a():
 
1860
    "Sample drawing, xvalue/ycat axes, y connected at right of x."
 
1861
 
 
1862
    drawing = Drawing(400, 200)
 
1863
 
 
1864
    data = [(10, 20, 30, 42)]
 
1865
 
 
1866
    xAxis = XValueAxis()
 
1867
    xAxis._length = 300
 
1868
    xAxis.configure(data)
 
1869
 
 
1870
    yAxis = YCategoryAxis()
 
1871
    yAxis.setPosition(50, 50, 125)
 
1872
    yAxis.joinAxis = xAxis
 
1873
    yAxis.joinAxisMode = 'right'
 
1874
    yAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni']
 
1875
    yAxis.labels.boxAnchor = 'e'
 
1876
    yAxis.configure(data)
 
1877
 
 
1878
    drawing.add(xAxis)
 
1879
    drawing.add(yAxis)
 
1880
 
 
1881
    return drawing
 
1882
 
 
1883
 
 
1884
def sample7b():
 
1885
    "Sample drawing, xvalue/ycat axes, y connected at left of x."
 
1886
 
 
1887
    drawing = Drawing(400, 200)
 
1888
 
 
1889
    data = [(10, 20, 30, 42)]
 
1890
 
 
1891
    xAxis = XValueAxis()
 
1892
    xAxis._length = 300
 
1893
    xAxis.configure(data)
 
1894
 
 
1895
    yAxis = YCategoryAxis()
 
1896
    yAxis.setPosition(50, 50, 125)
 
1897
    yAxis.joinAxis = xAxis
 
1898
    yAxis.joinAxisMode = 'left'
 
1899
    yAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni']
 
1900
    yAxis.labels.boxAnchor = 'e'
 
1901
    yAxis.configure(data)
 
1902
 
 
1903
    drawing.add(xAxis)
 
1904
    drawing.add(yAxis)
 
1905
 
 
1906
    return drawing
 
1907
 
 
1908
 
 
1909
def sample7c():
 
1910
    "Sample drawing, xvalue/ycat axes, y connected at value 30 of x."
 
1911
 
 
1912
    drawing = Drawing(400, 200)
 
1913
 
 
1914
    data = [(10, 20, 30, 42)]
 
1915
 
 
1916
    xAxis = XValueAxis()
 
1917
    xAxis._length = 300
 
1918
    xAxis.configure(data)
 
1919
 
 
1920
    yAxis = YCategoryAxis()
 
1921
    yAxis.setPosition(50, 50, 125)
 
1922
    yAxis.joinAxis = xAxis
 
1923
    yAxis.joinAxisMode = 'value'
 
1924
    yAxis.joinAxisPos = 30
 
1925
    yAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni']
 
1926
    yAxis.labels.boxAnchor = 'e'
 
1927
    yAxis.configure(data)
 
1928
 
 
1929
    drawing.add(xAxis)
 
1930
    drawing.add(yAxis)
 
1931
 
 
1932
    return drawing
 
1933
 
 
1934
 
 
1935
def sample7d():
 
1936
    "Sample drawing, xvalue/ycat axes, y connected at 200 pts to x."
 
1937
 
 
1938
    drawing = Drawing(400, 200)
 
1939
 
 
1940
    data = [(10, 20, 30, 42)]
 
1941
 
 
1942
    xAxis = XValueAxis()
 
1943
    xAxis._length = 300
 
1944
    xAxis.configure(data)
 
1945
 
 
1946
    yAxis = YCategoryAxis()
 
1947
    yAxis.setPosition(50, 50, 125)
 
1948
    yAxis.joinAxis = xAxis
 
1949
    yAxis.joinAxisMode = 'points'
 
1950
    yAxis.joinAxisPos = 200
 
1951
    yAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni']
 
1952
    yAxis.labels.boxAnchor = 'e'
 
1953
    yAxis.configure(data)
 
1954
 
 
1955
    drawing.add(xAxis)
 
1956
    drawing.add(yAxis)
 
1957
 
 
1958
    return drawing