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