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