/+junk/pygooglechart-py3k

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/%2Bjunk/pygooglechart-py3k
22 by gak
- pygooglechart.py converted to unix line breaks
1
"""
35 by gak
- Initial "grammar" code
2
pygooglechart - A complete Python wrapper for the Google Chart API
22 by gak
- pygooglechart.py converted to unix line breaks
3
4
http://pygooglechart.slowchop.com/
5
34 by gak
- initial unit tests
6
Copyright 2007-2008 Gerald Kaszuba
22 by gak
- pygooglechart.py converted to unix line breaks
7
8
This program is free software: you can redistribute it and/or modify
9
it under the terms of the GNU General Public License as published by
10
the Free Software Foundation, either version 3 of the License, or
11
(at your option) any later version.
12
13
This program is distributed in the hope that it will be useful,
14
but WITHOUT ANY WARRANTY; without even the implied warranty of
15
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
GNU General Public License for more details.
17
18
You should have received a copy of the GNU General Public License
19
along with this program.  If not, see <http://www.gnu.org/licenses/>.
20
21
"""
22
23
import os
24
import urllib
25
import urllib2
26
import math
27
import random
28
import re
36 by gak
- Really added initial unit tests
29
import warnings
30
import copy
22 by gak
- pygooglechart.py converted to unix line breaks
31
32
# Helper variables and functions
33
# -----------------------------------------------------------------------------
34
33 by gak
pep8 fixes, version bump
35
__version__ = '0.2.1'
35 by gak
- Initial "grammar" code
36
__author__ = 'Gerald Kaszuba'
22 by gak
- pygooglechart.py converted to unix line breaks
37
38
reo_colour = re.compile('^([A-Fa-f0-9]{2,2}){3,4}$')
39
40
def _check_colour(colour):
41
    if not reo_colour.match(colour):
42
        raise InvalidParametersException('Colours need to be in ' \
43
            'RRGGBB or RRGGBBAA format. One of your colours has %s' % \
44
            colour)
45
36 by gak
- Really added initial unit tests
46
47
def _reset_warnings():
48
    """Helper function to reset all warnings. Used by the unit tests."""
49
    globals()['__warningregistry__'] = None
50
#def _warn(message):
51
#    warnings.warn_explicit(msg, warnings.UserWarning,
52
53
22 by gak
- pygooglechart.py converted to unix line breaks
54
# Exception Classes
55
# -----------------------------------------------------------------------------
56
57
58
class PyGoogleChartException(Exception):
59
    pass
60
61
62
class DataOutOfRangeException(PyGoogleChartException):
63
    pass
64
65
66
class UnknownDataTypeException(PyGoogleChartException):
67
    pass
68
69
70
class NoDataGivenException(PyGoogleChartException):
71
    pass
72
73
74
class InvalidParametersException(PyGoogleChartException):
75
    pass
76
77
78
class BadContentTypeException(PyGoogleChartException):
79
    pass
80
81
35 by gak
- Initial "grammar" code
82
class AbstractClassException(PyGoogleChartException):
83
    pass
84
85
86
class UnknownChartType(PyGoogleChartException):
87
    pass
88
89
22 by gak
- pygooglechart.py converted to unix line breaks
90
# Data Classes
91
# -----------------------------------------------------------------------------
92
93
94
class Data(object):
95
96
    def __init__(self, data):
35 by gak
- Initial "grammar" code
97
        if type(self) == Data:
98
            raise AbstractClassException('This is an abstract class')
22 by gak
- pygooglechart.py converted to unix line breaks
99
        self.data = data
100
25 by gak
Autoscale fixes and refactoring by Grahm Ullrich
101
    @classmethod
102
    def float_scale_value(cls, value, range):
103
        lower, upper = range
34 by gak
- initial unit tests
104
        assert(upper > lower)
36 by gak
- Really added initial unit tests
105
        scaled = (value - lower) * (float(cls.max_value) / (upper - lower))
25 by gak
Autoscale fixes and refactoring by Grahm Ullrich
106
        return scaled
107
108
    @classmethod
109
    def clip_value(cls, value):
36 by gak
- Really added initial unit tests
110
        return max(0, min(value, cls.max_value))
25 by gak
Autoscale fixes and refactoring by Grahm Ullrich
111
112
    @classmethod
113
    def int_scale_value(cls, value, range):
34 by gak
- initial unit tests
114
        return int(round(cls.float_scale_value(value, range)))
25 by gak
Autoscale fixes and refactoring by Grahm Ullrich
115
116
    @classmethod
117
    def scale_value(cls, value, range):
118
        scaled = cls.int_scale_value(value, range)
119
        clipped = cls.clip_value(scaled)
36 by gak
- Really added initial unit tests
120
        if clipped != scaled:
121
            warnings.warn('One or more of of your data points has been '
122
                'clipped because it is out of range.')
25 by gak
Autoscale fixes and refactoring by Grahm Ullrich
123
        return clipped
22 by gak
- pygooglechart.py converted to unix line breaks
124
33 by gak
pep8 fixes, version bump
125
22 by gak
- pygooglechart.py converted to unix line breaks
126
class SimpleData(Data):
33 by gak
pep8 fixes, version bump
127
36 by gak
- Really added initial unit tests
128
    max_value = 61
22 by gak
- pygooglechart.py converted to unix line breaks
129
    enc_map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
130
131
    def __repr__(self):
132
        encoded_data = []
133
        for data in self.data:
134
            sub_data = []
135
            for value in data:
136
                if value is None:
137
                    sub_data.append('_')
36 by gak
- Really added initial unit tests
138
                elif value >= 0 and value <= self.max_value:
22 by gak
- pygooglechart.py converted to unix line breaks
139
                    sub_data.append(SimpleData.enc_map[value])
140
                else:
141
                    raise DataOutOfRangeException('cannot encode value: %d'
142
                                                  % value)
143
            encoded_data.append(''.join(sub_data))
144
        return 'chd=s:' + ','.join(encoded_data)
145
33 by gak
pep8 fixes, version bump
146
22 by gak
- pygooglechart.py converted to unix line breaks
147
class TextData(Data):
148
36 by gak
- Really added initial unit tests
149
    max_value = 100
150
22 by gak
- pygooglechart.py converted to unix line breaks
151
    def __repr__(self):
152
        encoded_data = []
153
        for data in self.data:
154
            sub_data = []
155
            for value in data:
156
                if value is None:
157
                    sub_data.append(-1)
36 by gak
- Really added initial unit tests
158
                elif value >= 0 and value <= self.max_value:
22 by gak
- pygooglechart.py converted to unix line breaks
159
                    sub_data.append("%.1f" % float(value))
160
                else:
161
                    raise DataOutOfRangeException()
162
            encoded_data.append(','.join(sub_data))
163
        return 'chd=t:' + '|'.join(encoded_data)
164
165
    @classmethod
166
    def scale_value(cls, value, range):
25 by gak
Autoscale fixes and refactoring by Grahm Ullrich
167
        # use float values instead of integers because we don't need an encode
168
        # map index
33 by gak
pep8 fixes, version bump
169
        scaled = cls.float_scale_value(value, range)
25 by gak
Autoscale fixes and refactoring by Grahm Ullrich
170
        clipped = cls.clip_value(scaled)
171
        return clipped
22 by gak
- pygooglechart.py converted to unix line breaks
172
33 by gak
pep8 fixes, version bump
173
22 by gak
- pygooglechart.py converted to unix line breaks
174
class ExtendedData(Data):
34 by gak
- initial unit tests
175
36 by gak
- Really added initial unit tests
176
    max_value = 4095
22 by gak
- pygooglechart.py converted to unix line breaks
177
    enc_map = \
178
        'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-.'
179
180
    def __repr__(self):
181
        encoded_data = []
182
        enc_size = len(ExtendedData.enc_map)
183
        for data in self.data:
184
            sub_data = []
185
            for value in data:
186
                if value is None:
187
                    sub_data.append('__')
36 by gak
- Really added initial unit tests
188
                elif value >= 0 and value <= self.max_value:
22 by gak
- pygooglechart.py converted to unix line breaks
189
                    first, second = divmod(int(value), enc_size)
190
                    sub_data.append('%s%s' % (
191
                        ExtendedData.enc_map[first],
192
                        ExtendedData.enc_map[second]))
193
                else:
194
                    raise DataOutOfRangeException( \
195
                        'Item #%i "%s" is out of range' % (data.index(value), \
196
                        value))
197
            encoded_data.append(''.join(sub_data))
198
        return 'chd=e:' + ','.join(encoded_data)
199
200
201
# Axis Classes
202
# -----------------------------------------------------------------------------
203
204
205
class Axis(object):
34 by gak
- initial unit tests
206
22 by gak
- pygooglechart.py converted to unix line breaks
207
    BOTTOM = 'x'
208
    TOP = 't'
209
    LEFT = 'y'
210
    RIGHT = 'r'
211
    TYPES = (BOTTOM, TOP, LEFT, RIGHT)
212
213
    def __init__(self, axis_index, axis_type, **kw):
214
        assert(axis_type in Axis.TYPES)
215
        self.has_style = False
216
        self.axis_index = axis_index
217
        self.axis_type = axis_type
218
        self.positions = None
219
220
    def set_index(self, axis_index):
221
        self.axis_index = axis_index
222
223
    def set_positions(self, positions):
224
        self.positions = positions
225
226
    def set_style(self, colour, font_size=None, alignment=None):
227
        _check_colour(colour)
228
        self.colour = colour
229
        self.font_size = font_size
230
        self.alignment = alignment
231
        self.has_style = True
232
233
    def style_to_url(self):
234
        bits = []
235
        bits.append(str(self.axis_index))
236
        bits.append(self.colour)
237
        if self.font_size is not None:
238
            bits.append(str(self.font_size))
239
            if self.alignment is not None:
240
                bits.append(str(self.alignment))
241
        return ','.join(bits)
242
243
    def positions_to_url(self):
244
        bits = []
245
        bits.append(str(self.axis_index))
246
        bits += [str(a) for a in self.positions]
247
        return ','.join(bits)
248
249
250
class LabelAxis(Axis):
251
252
    def __init__(self, axis_index, axis_type, values, **kwargs):
253
        Axis.__init__(self, axis_index, axis_type, **kwargs)
254
        self.values = [str(a) for a in values]
255
256
    def __repr__(self):
257
        return '%i:|%s' % (self.axis_index, '|'.join(self.values))
258
259
260
class RangeAxis(Axis):
261
262
    def __init__(self, axis_index, axis_type, low, high, **kwargs):
263
        Axis.__init__(self, axis_index, axis_type, **kwargs)
264
        self.low = low
265
        self.high = high
266
267
    def __repr__(self):
268
        return '%i,%s,%s' % (self.axis_index, self.low, self.high)
269
270
# Chart Classes
271
# -----------------------------------------------------------------------------
272
273
274
class Chart(object):
275
    """Abstract class for all chart types.
276
277
    width are height specify the dimensions of the image. title sets the title
278
    of the chart. legend requires a list that corresponds to datasets.
279
    """
280
281
    BASE_URL = 'http://chart.apis.google.com/chart?'
282
    BACKGROUND = 'bg'
283
    CHART = 'c'
30 by gak
Added zero line to bar charts
284
    ALPHA = 'a'
285
    VALID_SOLID_FILL_TYPES = (BACKGROUND, CHART, ALPHA)
22 by gak
- pygooglechart.py converted to unix line breaks
286
    SOLID = 's'
287
    LINEAR_GRADIENT = 'lg'
288
    LINEAR_STRIPES = 'ls'
289
290
    def __init__(self, width, height, title=None, legend=None, colours=None,
36 by gak
- Really added initial unit tests
291
            auto_scale=True, x_range=None, y_range=None):
35 by gak
- Initial "grammar" code
292
        if type(self) == Chart:
293
            raise AbstractClassException('This is an abstract class')
22 by gak
- pygooglechart.py converted to unix line breaks
294
        assert(isinstance(width, int))
295
        assert(isinstance(height, int))
296
        self.width = width
297
        self.height = height
298
        self.data = []
299
        self.set_title(title)
300
        self.set_legend(legend)
301
        self.set_colours(colours)
302
303
        # Data for scaling.
36 by gak
- Really added initial unit tests
304
        self.auto_scale = auto_scale  # Whether to automatically scale data
305
        self.x_range = x_range  # (min, max) x-axis range for scaling
306
        self.y_range = y_range  # (min, max) y-axis range for scaling
22 by gak
- pygooglechart.py converted to unix line breaks
307
        self.scaled_data_class = None
308
        self.scaled_x_range = None
309
        self.scaled_y_range = None
310
311
        self.fill_types = {
312
            Chart.BACKGROUND: None,
313
            Chart.CHART: None,
30 by gak
Added zero line to bar charts
314
            Chart.ALPHA: None,
22 by gak
- pygooglechart.py converted to unix line breaks
315
        }
316
        self.fill_area = {
317
            Chart.BACKGROUND: None,
318
            Chart.CHART: None,
30 by gak
Added zero line to bar charts
319
            Chart.ALPHA: None,
22 by gak
- pygooglechart.py converted to unix line breaks
320
        }
321
        self.axis = []
322
        self.markers = []
27 by gak
grids and line styles are not only restricted to line chart types
323
        self.line_styles = {}
324
        self.grid = None
22 by gak
- pygooglechart.py converted to unix line breaks
325
326
    # URL generation
327
    # -------------------------------------------------------------------------
328
25 by gak
Autoscale fixes and refactoring by Grahm Ullrich
329
    def get_url(self, data_class=None):
330
        url_bits = self.get_url_bits(data_class=data_class)
22 by gak
- pygooglechart.py converted to unix line breaks
331
        return self.BASE_URL + '&'.join(url_bits)
332
25 by gak
Autoscale fixes and refactoring by Grahm Ullrich
333
    def get_url_bits(self, data_class=None):
22 by gak
- pygooglechart.py converted to unix line breaks
334
        url_bits = []
335
        # required arguments
336
        url_bits.append(self.type_to_url())
337
        url_bits.append('chs=%ix%i' % (self.width, self.height))
25 by gak
Autoscale fixes and refactoring by Grahm Ullrich
338
        url_bits.append(self.data_to_url(data_class=data_class))
22 by gak
- pygooglechart.py converted to unix line breaks
339
        # optional arguments
340
        if self.title:
341
            url_bits.append('chtt=%s' % self.title)
342
        if self.legend:
343
            url_bits.append('chdl=%s' % '|'.join(self.legend))
344
        if self.colours:
345
            url_bits.append('chco=%s' % ','.join(self.colours))
346
        ret = self.fill_to_url()
347
        if ret:
348
            url_bits.append(ret)
349
        ret = self.axis_to_url()
350
        if ret:
351
            url_bits.append(ret)
352
        if self.markers:
353
            url_bits.append(self.markers_to_url())
27 by gak
grids and line styles are not only restricted to line chart types
354
        if self.line_styles:
355
            style = []
356
            for index in xrange(max(self.line_styles) + 1):
357
                if index in self.line_styles:
358
                    values = self.line_styles[index]
359
                else:
360
                    values = ('1', )
361
                style.append(','.join(values))
362
            url_bits.append('chls=%s' % '|'.join(style))
363
        if self.grid:
364
            url_bits.append('chg=%s' % self.grid)
22 by gak
- pygooglechart.py converted to unix line breaks
365
        return url_bits
366
367
    # Downloading
368
    # -------------------------------------------------------------------------
369
370
    def download(self, file_name):
371
        opener = urllib2.urlopen(self.get_url())
372
373
        if opener.headers['content-type'] != 'image/png':
374
            raise BadContentTypeException('Server responded with a ' \
375
                'content-type of %s' % opener.headers['content-type'])
376
377
        open(file_name, 'wb').write(urllib.urlopen(self.get_url()).read())
378
379
    # Simple settings
380
    # -------------------------------------------------------------------------
381
382
    def set_title(self, title):
383
        if title:
384
            self.title = urllib.quote(title)
385
        else:
386
            self.title = None
387
388
    def set_legend(self, legend):
389
        """legend needs to be a list, tuple or None"""
390
        assert(isinstance(legend, list) or isinstance(legend, tuple) or
391
            legend is None)
392
        if legend:
393
            self.legend = [urllib.quote(a) for a in legend]
394
        else:
395
            self.legend = None
396
397
    # Chart colours
398
    # -------------------------------------------------------------------------
399
400
    def set_colours(self, colours):
401
        # colours needs to be a list, tuple or None
402
        assert(isinstance(colours, list) or isinstance(colours, tuple) or
403
            colours is None)
404
        # make sure the colours are in the right format
405
        if colours:
406
            for col in colours:
407
                _check_colour(col)
408
        self.colours = colours
409
410
    # Background/Chart colours
411
    # -------------------------------------------------------------------------
412
413
    def fill_solid(self, area, colour):
30 by gak
Added zero line to bar charts
414
        assert(area in Chart.VALID_SOLID_FILL_TYPES)
22 by gak
- pygooglechart.py converted to unix line breaks
415
        _check_colour(colour)
416
        self.fill_area[area] = colour
417
        self.fill_types[area] = Chart.SOLID
418
419
    def _check_fill_linear(self, angle, *args):
420
        assert(isinstance(args, list) or isinstance(args, tuple))
421
        assert(angle >= 0 and angle <= 90)
422
        assert(len(args) % 2 == 0)
423
        args = list(args)  # args is probably a tuple and we need to mutate
424
        for a in xrange(len(args) / 2):
425
            col = args[a * 2]
426
            offset = args[a * 2 + 1]
427
            _check_colour(col)
428
            assert(offset >= 0 and offset <= 1)
429
            args[a * 2 + 1] = str(args[a * 2 + 1])
430
        return args
431
432
    def fill_linear_gradient(self, area, angle, *args):
30 by gak
Added zero line to bar charts
433
        assert(area in Chart.VALID_SOLID_FILL_TYPES)
22 by gak
- pygooglechart.py converted to unix line breaks
434
        args = self._check_fill_linear(angle, *args)
435
        self.fill_types[area] = Chart.LINEAR_GRADIENT
436
        self.fill_area[area] = ','.join([str(angle)] + args)
437
438
    def fill_linear_stripes(self, area, angle, *args):
30 by gak
Added zero line to bar charts
439
        assert(area in Chart.VALID_SOLID_FILL_TYPES)
22 by gak
- pygooglechart.py converted to unix line breaks
440
        args = self._check_fill_linear(angle, *args)
441
        self.fill_types[area] = Chart.LINEAR_STRIPES
442
        self.fill_area[area] = ','.join([str(angle)] + args)
443
444
    def fill_to_url(self):
445
        areas = []
30 by gak
Added zero line to bar charts
446
        for area in (Chart.BACKGROUND, Chart.CHART, Chart.ALPHA):
22 by gak
- pygooglechart.py converted to unix line breaks
447
            if self.fill_types[area]:
448
                areas.append('%s,%s,%s' % (area, self.fill_types[area], \
449
                    self.fill_area[area]))
450
        if areas:
451
            return 'chf=' + '|'.join(areas)
452
453
    # Data
454
    # -------------------------------------------------------------------------
455
456
    def data_class_detection(self, data):
457
        """Determines the appropriate data encoding type to give satisfactory
458
        resolution (http://code.google.com/apis/chart/#chart_data).
459
        """
460
        assert(isinstance(data, list) or isinstance(data, tuple))
461
        if not isinstance(self, (LineChart, BarChart, ScatterChart)):
462
            # From the link above:
463
            #   Simple encoding is suitable for all other types of chart
464
            #   regardless of size.
465
            return SimpleData
466
        elif self.height < 100:
467
            # The link above indicates that line and bar charts less
468
            # than 300px in size can be suitably represented with the
469
            # simple encoding. I've found that this isn't sufficient,
470
            # e.g. examples/line-xy-circle.png. Let's try 100px.
471
            return SimpleData
472
        else:
473
            return ExtendedData
474
475
    def data_x_range(self):
476
        """Return a 2-tuple giving the minimum and maximum x-axis
477
        data range.
478
        """
479
        try:
480
            lower = min([min(s) for type, s in self.annotated_data()
481
                         if type == 'x'])
482
            upper = max([max(s) for type, s in self.annotated_data()
483
                         if type == 'x'])
484
            return (lower, upper)
485
        except ValueError:
486
            return None     # no x-axis datasets
487
488
    def data_y_range(self):
489
        """Return a 2-tuple giving the minimum and maximum y-axis
490
        data range.
491
        """
492
        try:
493
            lower = min([min(s) for type, s in self.annotated_data()
494
                         if type == 'y'])
34 by gak
- initial unit tests
495
            upper = max([max(s) + 1 for type, s in self.annotated_data()
22 by gak
- pygooglechart.py converted to unix line breaks
496
                         if type == 'y'])
497
            return (lower, upper)
498
        except ValueError:
499
            return None     # no y-axis datasets
500
501
    def scaled_data(self, data_class, x_range=None, y_range=None):
502
        """Scale `self.data` as appropriate for the given data encoding
503
        (data_class) and return it.
504
505
        An optional `y_range` -- a 2-tuple (lower, upper) -- can be
506
        given to specify the y-axis bounds. If not given, the range is
507
        inferred from the data: (0, <max-value>) presuming no negative
508
        values, or (<min-value>, <max-value>) if there are negative
509
        values.  `self.scaled_y_range` is set to the actual lower and
510
        upper scaling range.
511
512
        Ditto for `x_range`. Note that some chart types don't have x-axis
513
        data.
514
        """
515
        self.scaled_data_class = data_class
516
517
        # Determine the x-axis range for scaling.
518
        if x_range is None:
519
            x_range = self.data_x_range()
520
            if x_range and x_range[0] > 0:
521
                x_range = (0, x_range[1])
522
        self.scaled_x_range = x_range
523
524
        # Determine the y-axis range for scaling.
525
        if y_range is None:
526
            y_range = self.data_y_range()
527
            if y_range and y_range[0] > 0:
528
                y_range = (0, y_range[1])
529
        self.scaled_y_range = y_range
530
531
        scaled_data = []
532
        for type, dataset in self.annotated_data():
533
            if type == 'x':
534
                scale_range = x_range
535
            elif type == 'y':
536
                scale_range = y_range
537
            elif type == 'marker-size':
538
                scale_range = (0, max(dataset))
539
            scaled_data.append([data_class.scale_value(v, scale_range)
540
                                for v in dataset])
541
        return scaled_data
542
543
    def add_data(self, data):
544
        self.data.append(data)
545
        return len(self.data) - 1  # return the "index" of the data set
546
547
    def data_to_url(self, data_class=None):
548
        if not data_class:
549
            data_class = self.data_class_detection(self.data)
550
        if not issubclass(data_class, Data):
551
            raise UnknownDataTypeException()
552
        if self.auto_scale:
553
            data = self.scaled_data(data_class, self.x_range, self.y_range)
554
        else:
555
            data = self.data
556
        return repr(data_class(data))
557
29 by gak
Added Google-o-meter chart
558
    def annotated_data(self):
559
        for dataset in self.data:
560
            yield ('x', dataset)
561
22 by gak
- pygooglechart.py converted to unix line breaks
562
    # Axis Labels
563
    # -------------------------------------------------------------------------
564
565
    def set_axis_labels(self, axis_type, values):
566
        assert(axis_type in Axis.TYPES)
33 by gak
pep8 fixes, version bump
567
        values = [urllib.quote(a) for a in values]
22 by gak
- pygooglechart.py converted to unix line breaks
568
        axis_index = len(self.axis)
569
        axis = LabelAxis(axis_index, axis_type, values)
570
        self.axis.append(axis)
571
        return axis_index
572
573
    def set_axis_range(self, axis_type, low, high):
574
        assert(axis_type in Axis.TYPES)
575
        axis_index = len(self.axis)
576
        axis = RangeAxis(axis_index, axis_type, low, high)
577
        self.axis.append(axis)
578
        return axis_index
579
580
    def set_axis_positions(self, axis_index, positions):
581
        try:
582
            self.axis[axis_index].set_positions(positions)
583
        except IndexError:
584
            raise InvalidParametersException('Axis index %i has not been ' \
585
                'created' % axis)
586
587
    def set_axis_style(self, axis_index, colour, font_size=None, \
588
            alignment=None):
589
        try:
590
            self.axis[axis_index].set_style(colour, font_size, alignment)
591
        except IndexError:
592
            raise InvalidParametersException('Axis index %i has not been ' \
593
                'created' % axis)
594
595
    def axis_to_url(self):
596
        available_axis = []
597
        label_axis = []
598
        range_axis = []
599
        positions = []
600
        styles = []
601
        index = -1
602
        for axis in self.axis:
603
            available_axis.append(axis.axis_type)
604
            if isinstance(axis, RangeAxis):
605
                range_axis.append(repr(axis))
606
            if isinstance(axis, LabelAxis):
607
                label_axis.append(repr(axis))
608
            if axis.positions:
609
                positions.append(axis.positions_to_url())
610
            if axis.has_style:
611
                styles.append(axis.style_to_url())
612
        if not available_axis:
613
            return
614
        url_bits = []
615
        url_bits.append('chxt=%s' % ','.join(available_axis))
616
        if label_axis:
617
            url_bits.append('chxl=%s' % '|'.join(label_axis))
618
        if range_axis:
619
            url_bits.append('chxr=%s' % '|'.join(range_axis))
620
        if positions:
621
            url_bits.append('chxp=%s' % '|'.join(positions))
622
        if styles:
623
            url_bits.append('chxs=%s' % '|'.join(styles))
624
        return '&'.join(url_bits)
625
626
    # Markers, Ranges and Fill area (chm)
627
    # -------------------------------------------------------------------------
628
629
    def markers_to_url(self):
630
        return 'chm=%s' % '|'.join([','.join(a) for a in self.markers])
631
31 by gak
Added priority to shape markers
632
    def add_marker(self, index, point, marker_type, colour, size, priority=0):
22 by gak
- pygooglechart.py converted to unix line breaks
633
        self.markers.append((marker_type, colour, str(index), str(point), \
31 by gak
Added priority to shape markers
634
            str(size), str(priority)))
22 by gak
- pygooglechart.py converted to unix line breaks
635
636
    def add_horizontal_range(self, colour, start, stop):
637
        self.markers.append(('r', colour, '1', str(start), str(stop)))
638
639
    def add_vertical_range(self, colour, start, stop):
640
        self.markers.append(('R', colour, '1', str(start), str(stop)))
641
642
    def add_fill_range(self, colour, index_start, index_end):
643
        self.markers.append(('b', colour, str(index_start), str(index_end), \
644
            '1'))
645
646
    def add_fill_simple(self, colour):
647
        self.markers.append(('B', colour, '1', '1', '1'))
648
27 by gak
grids and line styles are not only restricted to line chart types
649
    # Line styles
650
    # -------------------------------------------------------------------------
22 by gak
- pygooglechart.py converted to unix line breaks
651
652
    def set_line_style(self, index, thickness=1, line_segment=None, \
653
            blank_segment=None):
654
        value = []
655
        value.append(str(thickness))
656
        if line_segment:
657
            value.append(str(line_segment))
658
            value.append(str(blank_segment))
659
        self.line_styles[index] = value
660
27 by gak
grids and line styles are not only restricted to line chart types
661
    # Grid
662
    # -------------------------------------------------------------------------
663
22 by gak
- pygooglechart.py converted to unix line breaks
664
    def set_grid(self, x_step, y_step, line_segment=1, \
665
            blank_segment=0):
666
        self.grid = '%s,%s,%s,%s' % (x_step, y_step, line_segment, \
667
            blank_segment)
668
27 by gak
grids and line styles are not only restricted to line chart types
669
670
class ScatterChart(Chart):
671
672
    def type_to_url(self):
673
        return 'cht=s'
674
675
    def annotated_data(self):
676
        yield ('x', self.data[0])
677
        yield ('y', self.data[1])
678
        if len(self.data) > 2:
679
            # The optional third dataset is relative sizing for point
680
            # markers.
681
            yield ('marker-size', self.data[2])
682
33 by gak
pep8 fixes, version bump
683
27 by gak
grids and line styles are not only restricted to line chart types
684
class LineChart(Chart):
685
686
    def __init__(self, *args, **kwargs):
35 by gak
- Initial "grammar" code
687
        if type(self) == LineChart:
688
            raise AbstractClassException('This is an abstract class')
27 by gak
grids and line styles are not only restricted to line chart types
689
        Chart.__init__(self, *args, **kwargs)
690
22 by gak
- pygooglechart.py converted to unix line breaks
691
692
class SimpleLineChart(LineChart):
693
694
    def type_to_url(self):
695
        return 'cht=lc'
696
697
    def annotated_data(self):
698
        # All datasets are y-axis data.
699
        for dataset in self.data:
700
            yield ('y', dataset)
701
33 by gak
pep8 fixes, version bump
702
22 by gak
- pygooglechart.py converted to unix line breaks
703
class SparkLineChart(SimpleLineChart):
704
705
    def type_to_url(self):
706
        return 'cht=ls'
707
33 by gak
pep8 fixes, version bump
708
22 by gak
- pygooglechart.py converted to unix line breaks
709
class XYLineChart(LineChart):
710
711
    def type_to_url(self):
712
        return 'cht=lxy'
713
714
    def annotated_data(self):
715
        # Datasets alternate between x-axis, y-axis.
716
        for i, dataset in enumerate(self.data):
717
            if i % 2 == 0:
718
                yield ('x', dataset)
719
            else:
720
                yield ('y', dataset)
721
33 by gak
pep8 fixes, version bump
722
22 by gak
- pygooglechart.py converted to unix line breaks
723
class BarChart(Chart):
724
725
    def __init__(self, *args, **kwargs):
35 by gak
- Initial "grammar" code
726
        if type(self) == BarChart:
727
            raise AbstractClassException('This is an abstract class')
22 by gak
- pygooglechart.py converted to unix line breaks
728
        Chart.__init__(self, *args, **kwargs)
729
        self.bar_width = None
30 by gak
Added zero line to bar charts
730
        self.zero_lines = {}
22 by gak
- pygooglechart.py converted to unix line breaks
731
732
    def set_bar_width(self, bar_width):
733
        self.bar_width = bar_width
734
30 by gak
Added zero line to bar charts
735
    def set_zero_line(self, index, zero_line):
736
        self.zero_lines[index] = zero_line
737
738
    def get_url_bits(self, data_class=None, skip_chbh=False):
25 by gak
Autoscale fixes and refactoring by Grahm Ullrich
739
        url_bits = Chart.get_url_bits(self, data_class=data_class)
30 by gak
Added zero line to bar charts
740
        if not skip_chbh and self.bar_width is not None:
22 by gak
- pygooglechart.py converted to unix line breaks
741
            url_bits.append('chbh=%i' % self.bar_width)
30 by gak
Added zero line to bar charts
742
        zero_line = []
743
        if self.zero_lines:
744
            for index in xrange(max(self.zero_lines) + 1):
745
                if index in self.zero_lines:
746
                    zero_line.append(str(self.zero_lines[index]))
747
                else:
748
                    zero_line.append('0')
749
            url_bits.append('chp=%s' % ','.join(zero_line))
22 by gak
- pygooglechart.py converted to unix line breaks
750
        return url_bits
751
752
753
class StackedHorizontalBarChart(BarChart):
754
755
    def type_to_url(self):
756
        return 'cht=bhs'
757
758
759
class StackedVerticalBarChart(BarChart):
760
761
    def type_to_url(self):
762
        return 'cht=bvs'
763
764
    def annotated_data(self):
765
        for dataset in self.data:
766
            yield ('y', dataset)
767
768
769
class GroupedBarChart(BarChart):
770
771
    def __init__(self, *args, **kwargs):
35 by gak
- Initial "grammar" code
772
        if type(self) == GroupedBarChart:
773
            raise AbstractClassException('This is an abstract class')
22 by gak
- pygooglechart.py converted to unix line breaks
774
        BarChart.__init__(self, *args, **kwargs)
775
        self.bar_spacing = None
776
        self.group_spacing = None
777
778
    def set_bar_spacing(self, spacing):
779
        """Set spacing between bars in a group."""
780
        self.bar_spacing = spacing
781
782
    def set_group_spacing(self, spacing):
783
        """Set spacing between groups of bars."""
784
        self.group_spacing = spacing
785
25 by gak
Autoscale fixes and refactoring by Grahm Ullrich
786
    def get_url_bits(self, data_class=None):
22 by gak
- pygooglechart.py converted to unix line breaks
787
        # Skip 'BarChart.get_url_bits' and call Chart directly so the parent
788
        # doesn't add "chbh" before we do.
30 by gak
Added zero line to bar charts
789
        url_bits = BarChart.get_url_bits(self, data_class=data_class,
790
            skip_chbh=True)
22 by gak
- pygooglechart.py converted to unix line breaks
791
        if self.group_spacing is not None:
792
            if self.bar_spacing is None:
33 by gak
pep8 fixes, version bump
793
                raise InvalidParametersException('Bar spacing is required ' \
794
                    'to be set when setting group spacing')
22 by gak
- pygooglechart.py converted to unix line breaks
795
            if self.bar_width is None:
796
                raise InvalidParametersException('Bar width is required to ' \
797
                    'be set when setting bar spacing')
798
            url_bits.append('chbh=%i,%i,%i'
799
                % (self.bar_width, self.bar_spacing, self.group_spacing))
800
        elif self.bar_spacing is not None:
801
            if self.bar_width is None:
802
                raise InvalidParametersException('Bar width is required to ' \
803
                    'be set when setting bar spacing')
804
            url_bits.append('chbh=%i,%i' % (self.bar_width, self.bar_spacing))
27 by gak
grids and line styles are not only restricted to line chart types
805
        elif self.bar_width:
22 by gak
- pygooglechart.py converted to unix line breaks
806
            url_bits.append('chbh=%i' % self.bar_width)
807
        return url_bits
808
809
810
class GroupedHorizontalBarChart(GroupedBarChart):
811
812
    def type_to_url(self):
813
        return 'cht=bhg'
814
815
816
class GroupedVerticalBarChart(GroupedBarChart):
817
818
    def type_to_url(self):
819
        return 'cht=bvg'
820
821
    def annotated_data(self):
822
        for dataset in self.data:
823
            yield ('y', dataset)
824
825
826
class PieChart(Chart):
827
828
    def __init__(self, *args, **kwargs):
35 by gak
- Initial "grammar" code
829
        if type(self) == PieChart:
830
            raise AbstractClassException('This is an abstract class')
22 by gak
- pygooglechart.py converted to unix line breaks
831
        Chart.__init__(self, *args, **kwargs)
832
        self.pie_labels = []
36 by gak
- Really added initial unit tests
833
        if self.y_range:
834
            warnings.warn('y_range is not used with %s.' % \
835
                (self.__class__.__name__))
22 by gak
- pygooglechart.py converted to unix line breaks
836
837
    def set_pie_labels(self, labels):
838
        self.pie_labels = [urllib.quote(a) for a in labels]
839
25 by gak
Autoscale fixes and refactoring by Grahm Ullrich
840
    def get_url_bits(self, data_class=None):
841
        url_bits = Chart.get_url_bits(self, data_class=data_class)
22 by gak
- pygooglechart.py converted to unix line breaks
842
        if self.pie_labels:
843
            url_bits.append('chl=%s' % '|'.join(self.pie_labels))
844
        return url_bits
845
846
    def annotated_data(self):
847
        # Datasets are all y-axis data. However, there should only be
848
        # one dataset for pie charts.
849
        for dataset in self.data:
36 by gak
- Really added initial unit tests
850
            yield ('x', dataset)
22 by gak
- pygooglechart.py converted to unix line breaks
851
852
853
class PieChart2D(PieChart):
854
855
    def type_to_url(self):
856
        return 'cht=p'
857
858
859
class PieChart3D(PieChart):
860
861
    def type_to_url(self):
862
        return 'cht=p3'
863
864
865
class VennChart(Chart):
866
867
    def type_to_url(self):
868
        return 'cht=v'
869
870
    def annotated_data(self):
871
        for dataset in self.data:
872
            yield ('y', dataset)
873
874
26 by gak
Initial radar chart implementation
875
class RadarChart(Chart):
876
877
    def type_to_url(self):
878
        return 'cht=r'
879
33 by gak
pep8 fixes, version bump
880
28 by gak
Added map chart type
881
class SplineRadarChart(RadarChart):
882
883
    def type_to_url(self):
884
        return 'cht=rs'
885
886
887
class MapChart(Chart):
888
889
    def __init__(self, *args, **kwargs):
890
        Chart.__init__(self, *args, **kwargs)
891
        self.geo_area = 'world'
892
        self.codes = []
893
894
    def type_to_url(self):
895
        return 'cht=t'
896
897
    def set_codes(self, codes):
898
        self.codes = codes
899
900
    def get_url_bits(self, data_class=None):
901
        url_bits = Chart.get_url_bits(self, data_class=data_class)
902
        url_bits.append('chtm=%s' % self.geo_area)
903
        if self.codes:
904
            url_bits.append('chld=%s' % ''.join(self.codes))
905
        return url_bits
906
29 by gak
Added Google-o-meter chart
907
908
class GoogleOMeterChart(PieChart):
909
    """Inheriting from PieChart because of similar labeling"""
910
36 by gak
- Really added initial unit tests
911
    def __init__(self, *args, **kwargs):
912
        PieChart.__init__(self, *args, **kwargs)
913
        if self.auto_scale and not self.x_range:
914
            warnings.warn('Please specify an x_range with GoogleOMeterChart, '
915
                'otherwise one arrow will always be at the max.')
916
29 by gak
Added Google-o-meter chart
917
    def type_to_url(self):
918
        return 'cht=gom'
919
26 by gak
Initial radar chart implementation
920
35 by gak
- Initial "grammar" code
921
class ChartGrammar(object):
922
36 by gak
- Really added initial unit tests
923
    def __init__(self):
924
        self.grammar = None
925
        self.chart = None
926
927
    def parse(self, grammar):
35 by gak
- Initial "grammar" code
928
        self.grammar = grammar
929
        self.chart = self.create_chart_instance()
930
36 by gak
- Really added initial unit tests
931
        for attr in self.grammar:
932
            if attr in ('w', 'h', 'type', 'auto_scale', 'x_range', 'y_range'):
933
                continue  # These are already parsed in create_chart_instance
934
            attr_func = 'parse_' + attr
935
            if not hasattr(self, attr_func):
936
                warnings.warn('No parser for grammar attribute "%s"' % (attr))
937
                continue
938
            getattr(self, attr_func)(grammar[attr])
939
940
        return self.chart
941
942
    def parse_data(self, data):
943
        self.chart.data = data
944
        print self.chart.data
945
35 by gak
- Initial "grammar" code
946
    @staticmethod
947
    def get_possible_chart_types():
948
        possible_charts = []
36 by gak
- Really added initial unit tests
949
        for cls_name in globals().keys():
35 by gak
- Initial "grammar" code
950
            if not cls_name.endswith('Chart'):
951
                continue
952
            cls = globals()[cls_name]
953
            # Check if it is an abstract class
954
            try:
36 by gak
- Really added initial unit tests
955
                a = cls(1, 1, auto_scale=False)
956
                del a
35 by gak
- Initial "grammar" code
957
            except AbstractClassException:
958
                continue
959
            # Strip off "Class"
960
            possible_charts.append(cls_name[:-5])
961
        return possible_charts
962
36 by gak
- Really added initial unit tests
963
    def create_chart_instance(self, grammar=None):
964
        if not grammar:
965
            grammar = self.grammar
966
        assert(isinstance(grammar, dict))  # grammar must be a dict
35 by gak
- Initial "grammar" code
967
        assert('w' in grammar)  # width is required
968
        assert('h' in grammar)  # height is required
969
        assert('type' in grammar)  # type is required
36 by gak
- Really added initial unit tests
970
        chart_type = grammar['type']
971
        w = grammar['w']
972
        h = grammar['h']
973
        auto_scale = grammar.get('auto_scale', None)
974
        x_range = grammar.get('x_range', None)
975
        y_range = grammar.get('y_range', None)
35 by gak
- Initial "grammar" code
976
        types = ChartGrammar.get_possible_chart_types()
36 by gak
- Really added initial unit tests
977
        if chart_type not in types:
35 by gak
- Initial "grammar" code
978
            raise UnknownChartType('%s is an unknown chart type. Possible '
36 by gak
- Really added initial unit tests
979
                'chart types are %s' % (chart_type, ','.join(types)))
980
        return globals()[chart_type + 'Chart'](w, h, auto_scale=auto_scale,
981
            x_range=x_range, y_range=y_range)
35 by gak
- Initial "grammar" code
982
983
    def download(self):
984
        pass
985