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