/+junk/pygooglechart-py3k

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/%2Bjunk/pygooglechart-py3k

« back to all changes in this revision

Viewing changes to pygooglechart.py

  • Committer: Gustav Hartvigsson
  • Date: 2011-01-03 21:57:12 UTC
  • Revision ID: gustav.hartvigsson@gmail.com-20110103215712-1yeiw9tl7oiwh8w1
forgot the the the images in the examples folder...

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
"""
2
 
PyGoogleChart - A complete Python wrapper for the Google Chart API
 
2
pygooglechart - A complete Python wrapper for the Google Chart API
3
3
 
4
4
http://pygooglechart.slowchop.com/
5
5
 
6
 
Copyright 2007 Gerald Kaszuba
 
6
Copyright 2007-2009 Gerald Kaszuba
7
7
 
8
8
This program is free software: you can redistribute it and/or modify
9
9
it under the terms of the GNU General Public License as published by
19
19
along with this program.  If not, see <http://www.gnu.org/licenses/>.
20
20
 
21
21
"""
 
22
from __future__ import division
22
23
 
23
24
import os
24
25
import urllib
25
 
import urllib2
 
26
import urllib.request, urllib.error
26
27
import math
27
28
import random
28
29
import re
 
30
import warnings
 
31
import copy
29
32
 
30
33
# Helper variables and functions
31
34
# -----------------------------------------------------------------------------
32
35
 
33
 
__version__ = '0.2.0'
 
36
__version__ = '0.3.0'
 
37
__author__ = 'Gerald Kaszuba'
34
38
 
35
39
reo_colour = re.compile('^([A-Fa-f0-9]{2,2}){3,4}$')
36
40
 
37
 
 
38
41
def _check_colour(colour):
39
42
    if not reo_colour.match(colour):
40
43
        raise InvalidParametersException('Colours need to be in ' \
41
44
            'RRGGBB or RRGGBBAA format. One of your colours has %s' % \
42
45
            colour)
43
46
 
 
47
 
 
48
def _reset_warnings():
 
49
    """Helper function to reset all warnings. Used by the unit tests."""
 
50
    globals()['__warningregistry__'] = None
 
51
 
 
52
 
44
53
# Exception Classes
45
54
# -----------------------------------------------------------------------------
46
55
 
69
78
    pass
70
79
 
71
80
 
 
81
class AbstractClassException(PyGoogleChartException):
 
82
    pass
 
83
 
 
84
 
 
85
class UnknownChartType(PyGoogleChartException):
 
86
    pass
 
87
 
 
88
class UnknownCountryCodeException(PyGoogleChartException):
 
89
    pass
 
90
 
72
91
# Data Classes
73
92
# -----------------------------------------------------------------------------
74
93
 
76
95
class Data(object):
77
96
 
78
97
    def __init__(self, data):
79
 
        assert(type(self) != Data)  # This is an abstract class
 
98
        if type(self) == Data:
 
99
            raise AbstractClassException('This is an abstract class')
80
100
        self.data = data
81
101
 
82
102
    @classmethod
83
103
    def float_scale_value(cls, value, range):
84
104
        lower, upper = range
85
 
        max_value = cls.max_value()
86
 
        scaled = (value-lower) * (float(max_value)/(upper-lower))
 
105
        assert(upper > lower)
 
106
        scaled = (value - lower) * (cls.max_value / (upper - lower))
87
107
        return scaled
88
108
 
89
109
    @classmethod
90
110
    def clip_value(cls, value):
91
 
        clipped = max(0, min(value, cls.max_value()))
92
 
        return clipped
 
111
        return max(0, min(value, cls.max_value))
93
112
 
94
113
    @classmethod
95
114
    def int_scale_value(cls, value, range):
96
 
        scaled = int(round(cls.float_scale_value(value, range)))
97
 
        return scaled
 
115
        return int(round(cls.float_scale_value(value, range)))
98
116
 
99
117
    @classmethod
100
118
    def scale_value(cls, value, range):
101
119
        scaled = cls.int_scale_value(value, range)
102
120
        clipped = cls.clip_value(scaled)
 
121
        Data.check_clip(scaled, clipped)
103
122
        return clipped
104
123
 
 
124
    @staticmethod
 
125
    def check_clip(scaled, clipped):
 
126
        if clipped != scaled:
 
127
            warnings.warn('One or more of of your data points has been '
 
128
                'clipped because it is out of range.')
 
129
 
 
130
 
105
131
class SimpleData(Data):
 
132
 
 
133
    max_value = 61
106
134
    enc_map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
107
135
 
108
136
    def __repr__(self):
109
 
        max_value = self.max_value()
110
137
        encoded_data = []
111
138
        for data in self.data:
112
139
            sub_data = []
113
140
            for value in data:
114
141
                if value is None:
115
142
                    sub_data.append('_')
116
 
                elif value >= 0 and value <= max_value:
 
143
                elif value >= 0 and value <= self.max_value:
117
144
                    sub_data.append(SimpleData.enc_map[value])
118
145
                else:
119
146
                    raise DataOutOfRangeException('cannot encode value: %d'
121
148
            encoded_data.append(''.join(sub_data))
122
149
        return 'chd=s:' + ','.join(encoded_data)
123
150
 
124
 
    @staticmethod
125
 
    def max_value():
126
 
        return 61
127
151
 
128
152
class TextData(Data):
129
153
 
 
154
    max_value = 100
 
155
 
130
156
    def __repr__(self):
131
 
        max_value = self.max_value()
132
157
        encoded_data = []
133
158
        for data in self.data:
134
159
            sub_data = []
135
160
            for value in data:
136
161
                if value is None:
137
162
                    sub_data.append(-1)
138
 
                elif value >= 0 and value <= max_value:
 
163
                elif value >= 0 and value <= self.max_value:
139
164
                    sub_data.append("%.1f" % float(value))
140
165
                else:
141
166
                    raise DataOutOfRangeException()
142
167
            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
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
 
168
        return 'chd=t:' + '%7c'.join(encoded_data)
159
169
 
160
170
    @classmethod
161
171
    def scale_value(cls, value, range):
162
172
        # use float values instead of integers because we don't need an encode
163
173
        # map index
164
 
        scaled = cls.float_scale_value(value,range)
 
174
        scaled = cls.float_scale_value(value, range)
165
175
        clipped = cls.clip_value(scaled)
 
176
        Data.check_clip(scaled, clipped)
166
177
        return clipped
167
178
 
 
179
 
168
180
class ExtendedData(Data):
 
181
 
 
182
    max_value = 4095
169
183
    enc_map = \
170
184
        'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-.'
171
185
 
172
186
    def __repr__(self):
173
 
        max_value = self.max_value()
174
187
        encoded_data = []
175
188
        enc_size = len(ExtendedData.enc_map)
176
189
        for data in self.data:
178
191
            for value in data:
179
192
                if value is None:
180
193
                    sub_data.append('__')
181
 
                elif value >= 0 and value <= max_value:
 
194
                elif value >= 0 and value <= self.max_value:
182
195
                    first, second = divmod(int(value), enc_size)
183
196
                    sub_data.append('%s%s' % (
184
197
                        ExtendedData.enc_map[first],
190
203
            encoded_data.append(''.join(sub_data))
191
204
        return 'chd=e:' + ','.join(encoded_data)
192
205
 
193
 
    @staticmethod
194
 
    def max_value():
195
 
        return 4095
196
 
 
197
206
 
198
207
# Axis Classes
199
208
# -----------------------------------------------------------------------------
200
209
 
201
210
 
202
211
class Axis(object):
 
212
 
203
213
    BOTTOM = 'x'
204
214
    TOP = 't'
205
215
    LEFT = 'y'
250
260
        self.values = [str(a) for a in values]
251
261
 
252
262
    def __repr__(self):
253
 
        return '%i:|%s' % (self.axis_index, '|'.join(self.values))
 
263
        return '%i:%%7c%s' % (self.axis_index, '%7c'.join(self.values))
254
264
 
255
265
 
256
266
class RangeAxis(Axis):
274
284
    of the chart. legend requires a list that corresponds to datasets.
275
285
    """
276
286
 
277
 
    BASE_URL = 'http://chart.apis.google.com/chart?'
 
287
    BASE_URL = 'http://chart.apis.google.com/chart'
278
288
    BACKGROUND = 'bg'
279
289
    CHART = 'c'
280
290
    ALPHA = 'a'
284
294
    LINEAR_STRIPES = 'ls'
285
295
 
286
296
    def __init__(self, width, height, title=None, legend=None, colours=None,
287
 
                 auto_scale=True, x_range=None, y_range=None):
288
 
        assert(type(self) != Chart)  # This is an abstract class
 
297
            auto_scale=True, x_range=None, y_range=None,
 
298
            colours_within_series=None):
 
299
        if type(self) == Chart:
 
300
            raise AbstractClassException('This is an abstract class')
289
301
        assert(isinstance(width, int))
290
302
        assert(isinstance(height, int))
291
303
        self.width = width
292
304
        self.height = height
293
305
        self.data = []
294
306
        self.set_title(title)
 
307
        self.set_title_style(None, None)
295
308
        self.set_legend(legend)
 
309
        self.set_legend_position(None)
296
310
        self.set_colours(colours)
 
311
        self.set_colours_within_series(colours_within_series)
297
312
 
298
313
        # Data for scaling.
299
 
        self.auto_scale = auto_scale    # Whether to automatically scale data
300
 
        self.x_range = x_range          # (min, max) x-axis range for scaling
301
 
        self.y_range = y_range          # (min, max) y-axis range for scaling
 
314
        self.auto_scale = auto_scale  # Whether to automatically scale data
 
315
        self.x_range = x_range  # (min, max) x-axis range for scaling
 
316
        self.y_range = y_range  # (min, max) y-axis range for scaling
302
317
        self.scaled_data_class = None
303
318
        self.scaled_x_range = None
304
319
        self.scaled_y_range = None
317
332
        self.markers = []
318
333
        self.line_styles = {}
319
334
        self.grid = None
 
335
        self.title_colour = None
 
336
        self.title_font_size = None
320
337
 
321
338
    # URL generation
322
339
    # -------------------------------------------------------------------------
323
 
 
 
340
        
324
341
    def get_url(self, data_class=None):
 
342
        return self.BASE_URL + '?' + self.get_url_extension(data_class)
 
343
    
 
344
    def get_url_extension(self, data_class=None):
325
345
        url_bits = self.get_url_bits(data_class=data_class)
326
 
        return self.BASE_URL + '&'.join(url_bits)
 
346
        return '&'.join(url_bits)
327
347
 
328
348
    def get_url_bits(self, data_class=None):
329
349
        url_bits = []
334
354
        # optional arguments
335
355
        if self.title:
336
356
            url_bits.append('chtt=%s' % self.title)
 
357
        if self.title_colour and self.title_font_size:
 
358
            url_bits.append('chts=%s,%s' % (self.title_colour, \
 
359
                self.title_font_size))
337
360
        if self.legend:
338
 
            url_bits.append('chdl=%s' % '|'.join(self.legend))
 
361
            url_bits.append('chdl=%s' % '%7c'.join(self.legend))
 
362
        if self.legend_position:
 
363
            url_bits.append('chdlp=%s' % (self.legend_position))
339
364
        if self.colours:
340
 
            url_bits.append('chco=%s' % ','.join(self.colours))
 
365
            url_bits.append('chco=%s' % ','.join(self.colours))            
 
366
        if self.colours_within_series:
 
367
            url_bits.append('chco=%s' % '%7c'.join(self.colours_within_series))
341
368
        ret = self.fill_to_url()
342
369
        if ret:
343
370
            url_bits.append(ret)
344
371
        ret = self.axis_to_url()
345
372
        if ret:
346
 
            url_bits.append(ret)
 
373
            url_bits.append(ret)                    
347
374
        if self.markers:
348
 
            url_bits.append(self.markers_to_url())
 
375
            url_bits.append(self.markers_to_url())        
349
376
        if self.line_styles:
350
377
            style = []
351
 
            for index in xrange(max(self.line_styles) + 1):
 
378
            for index in range(max(self.line_styles) + 1):
352
379
                if index in self.line_styles:
353
380
                    values = self.line_styles[index]
354
381
                else:
355
382
                    values = ('1', )
356
383
                style.append(','.join(values))
357
 
            url_bits.append('chls=%s' % '|'.join(style))
 
384
            url_bits.append('chls=%s' % '%7c'.join(style))
358
385
        if self.grid:
359
386
            url_bits.append('chg=%s' % self.grid)
360
387
        return url_bits
362
389
    # Downloading
363
390
    # -------------------------------------------------------------------------
364
391
 
365
 
    def download(self, file_name):
366
 
        opener = urllib2.urlopen(self.get_url())
 
392
    def download(self, file_name, use_post=True):
 
393
        if use_post:
 
394
            opener = urllib.request.urlopen(self.BASE_URL, self.get_url_extension())
 
395
        else:
 
396
            opener = urllib.request.urlopen(self.get_url())
367
397
 
368
398
        if opener.headers['content-type'] != 'image/png':
369
399
            raise BadContentTypeException('Server responded with a ' \
370
400
                'content-type of %s' % opener.headers['content-type'])
371
401
 
372
 
        open(file_name, 'wb').write(urllib.urlopen(self.get_url()).read())
 
402
        open(file_name, 'wb').write(opener.read())
373
403
 
374
404
    # Simple settings
375
405
    # -------------------------------------------------------------------------
376
406
 
377
407
    def set_title(self, title):
378
408
        if title:
379
 
            self.title = urllib.quote(title)
 
409
            self.title = urllib.parse.quote(title)
380
410
        else:
381
411
            self.title = None
382
412
 
 
413
    def set_title_style(self, colour=None, font_size=None):
 
414
        if not colour is None:
 
415
            _check_colour(colour)
 
416
        if not colour and not font_size:
 
417
            return
 
418
        self.title_colour = colour or '333333'
 
419
        self.title_font_size = font_size or 13.5
 
420
 
383
421
    def set_legend(self, legend):
384
422
        """legend needs to be a list, tuple or None"""
385
423
        assert(isinstance(legend, list) or isinstance(legend, tuple) or
386
424
            legend is None)
387
425
        if legend:
388
 
            self.legend = [urllib.quote(a) for a in legend]
 
426
            self.legend = [urllib.parse.quote(a) for a in legend]
389
427
        else:
390
428
            self.legend = None
391
429
 
 
430
    def set_legend_position(self, legend_position):
 
431
        if legend_position:
 
432
            self.legend_position = urllib.parse.quote(legend_position)
 
433
        else:    
 
434
            self.legend_position = None
 
435
 
392
436
    # Chart colours
393
437
    # -------------------------------------------------------------------------
394
438
 
402
446
                _check_colour(col)
403
447
        self.colours = colours
404
448
 
 
449
    def set_colours_within_series(self, colours):
 
450
        # colours needs to be a list, tuple or None
 
451
        assert(isinstance(colours, list) or isinstance(colours, tuple) or
 
452
            colours is None)
 
453
        # make sure the colours are in the right format
 
454
        if colours:
 
455
            for col in colours:
 
456
                _check_colour(col)
 
457
        self.colours_within_series = colours        
 
458
 
405
459
    # Background/Chart colours
406
460
    # -------------------------------------------------------------------------
407
461
 
416
470
        assert(angle >= 0 and angle <= 90)
417
471
        assert(len(args) % 2 == 0)
418
472
        args = list(args)  # args is probably a tuple and we need to mutate
419
 
        for a in xrange(len(args) / 2):
 
473
        for a in range(int(len(args) / 2)):
420
474
            col = args[a * 2]
421
475
            offset = args[a * 2 + 1]
422
476
            _check_colour(col)
443
497
                areas.append('%s,%s,%s' % (area, self.fill_types[area], \
444
498
                    self.fill_area[area]))
445
499
        if areas:
446
 
            return 'chf=' + '|'.join(areas)
 
500
            return 'chf=' + '%7c'.join(areas)
447
501
 
448
502
    # Data
449
503
    # -------------------------------------------------------------------------
467
521
        else:
468
522
            return ExtendedData
469
523
 
 
524
    def _filter_none(self, data):
 
525
        return [r for r in data if r is not None]
 
526
 
470
527
    def data_x_range(self):
471
528
        """Return a 2-tuple giving the minimum and maximum x-axis
472
529
        data range.
473
530
        """
474
531
        try:
475
 
            lower = min([min(s) for type, s in self.annotated_data()
 
532
            lower = min([min(self._filter_none(s))
 
533
                         for type, s in self.annotated_data()
476
534
                         if type == 'x'])
477
 
            upper = max([max(s) for type, s in self.annotated_data()
 
535
            upper = max([max(self._filter_none(s))
 
536
                         for type, s in self.annotated_data()
478
537
                         if type == 'x'])
479
538
            return (lower, upper)
480
539
        except ValueError:
485
544
        data range.
486
545
        """
487
546
        try:
488
 
            lower = min([min(s) for type, s in self.annotated_data()
 
547
            lower = min([min(self._filter_none(s))
 
548
                         for type, s in self.annotated_data()
489
549
                         if type == 'y'])
490
 
            upper = max([max(s) for type, s in self.annotated_data()
 
550
            upper = max([max(self._filter_none(s)) + 1
 
551
                         for type, s in self.annotated_data()
491
552
                         if type == 'y'])
492
553
            return (lower, upper)
493
554
        except ValueError:
513
574
        if x_range is None:
514
575
            x_range = self.data_x_range()
515
576
            if x_range and x_range[0] > 0:
516
 
                x_range = (0, x_range[1])
 
577
                x_range = (x_range[0], x_range[1])
517
578
        self.scaled_x_range = x_range
518
579
 
519
580
        # Determine the y-axis range for scaling.
520
581
        if y_range is None:
521
582
            y_range = self.data_y_range()
522
583
            if y_range and y_range[0] > 0:
523
 
                y_range = (0, y_range[1])
 
584
                y_range = (y_range[0], y_range[1])
524
585
        self.scaled_y_range = y_range
525
586
 
526
587
        scaled_data = []
531
592
                scale_range = y_range
532
593
            elif type == 'marker-size':
533
594
                scale_range = (0, max(dataset))
534
 
            scaled_data.append([data_class.scale_value(v, scale_range)
535
 
                                for v in dataset])
 
595
            scaled_dataset = []
 
596
            for v in dataset:
 
597
                if v is None:
 
598
                    scaled_dataset.append(None)
 
599
                else:
 
600
                    scaled_dataset.append(
 
601
                        data_class.scale_value(v, scale_range))
 
602
            scaled_data.append(scaled_dataset)
536
603
        return scaled_data
537
604
 
538
605
    def add_data(self, data):
559
626
 
560
627
    def set_axis_labels(self, axis_type, values):
561
628
        assert(axis_type in Axis.TYPES)
562
 
        values = [ urllib.quote(a) for a in values ]
 
629
        values = [urllib.parse.quote(str(a)) for a in values]
563
630
        axis_index = len(self.axis)
564
631
        axis = LabelAxis(axis_index, axis_type, values)
565
632
        self.axis.append(axis)
609
676
        url_bits = []
610
677
        url_bits.append('chxt=%s' % ','.join(available_axis))
611
678
        if label_axis:
612
 
            url_bits.append('chxl=%s' % '|'.join(label_axis))
 
679
            url_bits.append('chxl=%s' % '%7c'.join(label_axis))
613
680
        if range_axis:
614
 
            url_bits.append('chxr=%s' % '|'.join(range_axis))
 
681
            url_bits.append('chxr=%s' % '%7c'.join(range_axis))
615
682
        if positions:
616
 
            url_bits.append('chxp=%s' % '|'.join(positions))
 
683
            url_bits.append('chxp=%s' % '%7c'.join(positions))
617
684
        if styles:
618
 
            url_bits.append('chxs=%s' % '|'.join(styles))
 
685
            url_bits.append('chxs=%s' % '%7c'.join(styles))
619
686
        return '&'.join(url_bits)
620
687
 
621
688
    # Markers, Ranges and Fill area (chm)
622
689
    # -------------------------------------------------------------------------
623
690
 
624
 
    def markers_to_url(self):
625
 
        return 'chm=%s' % '|'.join([','.join(a) for a in self.markers])
 
691
    def markers_to_url(self):        
 
692
        return 'chm=%s' % '%7c'.join([','.join(a) for a in self.markers])
626
693
 
627
694
    def add_marker(self, index, point, marker_type, colour, size, priority=0):
628
695
        self.markers.append((marker_type, colour, str(index), str(point), \
629
696
            str(size), str(priority)))
630
697
 
631
698
    def add_horizontal_range(self, colour, start, stop):
632
 
        self.markers.append(('r', colour, '1', str(start), str(stop)))
 
699
        self.markers.append(('r', colour, '0', str(start), str(stop)))
 
700
 
 
701
    def add_data_line(self, colour, data_set, size, priority=0):
 
702
        self.markers.append(('D', colour, str(data_set), '0', str(size), \
 
703
            str(priority)))
 
704
 
 
705
    def add_marker_text(self, string, colour, data_set, data_point, size, \
 
706
            priority=0):
 
707
        self.markers.append((str(string), colour, str(data_set), \
 
708
            str(data_point), str(size), str(priority)))        
633
709
 
634
710
    def add_vertical_range(self, colour, start, stop):
635
 
        self.markers.append(('R', colour, '1', str(start), str(stop)))
 
711
        self.markers.append(('R', colour, '0', str(start), str(stop)))
636
712
 
637
713
    def add_fill_range(self, colour, index_start, index_end):
638
714
        self.markers.append(('b', colour, str(index_start), str(index_end), \
675
751
            # markers.
676
752
            yield ('marker-size', self.data[2])
677
753
 
 
754
 
678
755
class LineChart(Chart):
679
756
 
680
757
    def __init__(self, *args, **kwargs):
681
 
        assert(type(self) != LineChart)  # This is an abstract class
 
758
        if type(self) == LineChart:
 
759
            raise AbstractClassException('This is an abstract class')
682
760
        Chart.__init__(self, *args, **kwargs)
683
761
 
684
 
#    def get_url_bits(self, data_class=None):
685
 
#        url_bits = Chart.get_url_bits(self, data_class=data_class)
686
 
#        return url_bits
687
 
 
688
762
 
689
763
class SimpleLineChart(LineChart):
690
764
 
696
770
        for dataset in self.data:
697
771
            yield ('y', dataset)
698
772
 
 
773
 
699
774
class SparkLineChart(SimpleLineChart):
700
775
 
701
776
    def type_to_url(self):
702
777
        return 'cht=ls'
703
778
 
 
779
 
704
780
class XYLineChart(LineChart):
705
781
 
706
782
    def type_to_url(self):
714
790
            else:
715
791
                yield ('y', dataset)
716
792
 
 
793
 
717
794
class BarChart(Chart):
718
795
 
719
796
    def __init__(self, *args, **kwargs):
720
 
        assert(type(self) != BarChart)  # This is an abstract class
 
797
        if type(self) == BarChart:
 
798
            raise AbstractClassException('This is an abstract class')
721
799
        Chart.__init__(self, *args, **kwargs)
722
800
        self.bar_width = None
723
801
        self.zero_lines = {}
734
812
            url_bits.append('chbh=%i' % self.bar_width)
735
813
        zero_line = []
736
814
        if self.zero_lines:
737
 
            for index in xrange(max(self.zero_lines) + 1):
 
815
            for index in range(max(self.zero_lines) + 1):
738
816
                if index in self.zero_lines:
739
817
                    zero_line.append(str(self.zero_lines[index]))
740
818
                else:
762
840
class GroupedBarChart(BarChart):
763
841
 
764
842
    def __init__(self, *args, **kwargs):
765
 
        assert(type(self) != GroupedBarChart)  # This is an abstract class
 
843
        if type(self) == GroupedBarChart:
 
844
            raise AbstractClassException('This is an abstract class')
766
845
        BarChart.__init__(self, *args, **kwargs)
767
846
        self.bar_spacing = None
768
847
        self.group_spacing = None
782
861
            skip_chbh=True)
783
862
        if self.group_spacing is not None:
784
863
            if self.bar_spacing is None:
785
 
                raise InvalidParametersException('Bar spacing is required to ' \
786
 
                    'be set when setting group spacing')
 
864
                raise InvalidParametersException('Bar spacing is required ' \
 
865
                    'to be set when setting group spacing')
787
866
            if self.bar_width is None:
788
867
                raise InvalidParametersException('Bar width is required to ' \
789
868
                    'be set when setting bar spacing')
818
897
class PieChart(Chart):
819
898
 
820
899
    def __init__(self, *args, **kwargs):
821
 
        assert(type(self) != PieChart)  # This is an abstract class
 
900
        if type(self) == PieChart:
 
901
            raise AbstractClassException('This is an abstract class')
822
902
        Chart.__init__(self, *args, **kwargs)
823
903
        self.pie_labels = []
 
904
        if self.y_range:
 
905
            warnings.warn('y_range is not used with %s.' % \
 
906
                (self.__class__.__name__))
824
907
 
825
908
    def set_pie_labels(self, labels):
826
 
        self.pie_labels = [urllib.quote(a) for a in labels]
 
909
        self.pie_labels = [urllib.parse.quote(a) for a in labels]
827
910
 
828
911
    def get_url_bits(self, data_class=None):
829
912
        url_bits = Chart.get_url_bits(self, data_class=data_class)
830
913
        if self.pie_labels:
831
 
            url_bits.append('chl=%s' % '|'.join(self.pie_labels))
 
914
            url_bits.append('chl=%s' % '%7c'.join(self.pie_labels))
832
915
        return url_bits
833
916
 
834
917
    def annotated_data(self):
835
918
        # Datasets are all y-axis data. However, there should only be
836
919
        # one dataset for pie charts.
837
920
        for dataset in self.data:
838
 
            yield ('y', dataset)
 
921
            yield ('x', dataset)
 
922
 
 
923
    def scaled_data(self, data_class, x_range=None, y_range=None):
 
924
        if not x_range:
 
925
            x_range = [0, sum(self.data[0])]
 
926
        return Chart.scaled_data(self, data_class, x_range, self.y_range)
839
927
 
840
928
 
841
929
class PieChart2D(PieChart):
865
953
    def type_to_url(self):
866
954
        return 'cht=r'
867
955
 
 
956
 
868
957
class SplineRadarChart(RadarChart):
869
958
 
870
959
    def type_to_url(self):
877
966
        Chart.__init__(self, *args, **kwargs)
878
967
        self.geo_area = 'world'
879
968
        self.codes = []
880
 
 
 
969
        self.__areas = ('africa', 'asia', 'europe', 'middle_east',
 
970
            'south_america', 'usa', 'world')
 
971
        self.__ccodes = (
 
972
            'AD', 'AE', 'AF', 'AG', 'AI', 'AL', 'AM', 'AN', 'AO', 'AQ', 'AR',
 
973
            'AS', 'AT', 'AU', 'AW', 'AX', 'AZ', 'BA', 'BB', 'BD', 'BE', 'BF',
 
974
            'BG', 'BH', 'BI', 'BJ', 'BL', 'BM', 'BN', 'BO', 'BR', 'BS', 'BT',
 
975
            'BV', 'BW', 'BY', 'BZ', 'CA', 'CC', 'CD', 'CF', 'CG', 'CH', 'CI',
 
976
            'CK', 'CL', 'CM', 'CN', 'CO', 'CR', 'CU', 'CV', 'CX', 'CY', 'CZ',
 
977
            'DE', 'DJ', 'DK', 'DM', 'DO', 'DZ', 'EC', 'EE', 'EG', 'EH', 'ER',
 
978
            'ES', 'ET', 'FI', 'FJ', 'FK', 'FM', 'FO', 'FR', 'GA', 'GB', 'GD',
 
979
            'GE', 'GF', 'GG', 'GH', 'GI', 'GL', 'GM', 'GN', 'GP', 'GQ', 'GR',
 
980
            'GS', 'GT', 'GU', 'GW', 'GY', 'HK', 'HM', 'HN', 'HR', 'HT', 'HU',
 
981
            'ID', 'IE', 'IL', 'IM', 'IN', 'IO', 'IQ', 'IR', 'IS', 'IT', 'JE',
 
982
            'JM', 'JO', 'JP', 'KE', 'KG', 'KH', 'KI', 'KM', 'KN', 'KP', 'KR',
 
983
            'KW', 'KY', 'KZ', 'LA', 'LB', 'LC', 'LI', 'LK', 'LR', 'LS', 'LT',
 
984
            'LU', 'LV', 'LY', 'MA', 'MC', 'MD', 'ME', 'MF', 'MG', 'MH', 'MK',
 
985
            'ML', 'MM', 'MN', 'MO', 'MP', 'MQ', 'MR', 'MS', 'MT', 'MU', 'MV',
 
986
            'MW', 'MX', 'MY', 'MZ', 'NA', 'NC', 'NE', 'NF', 'NG', 'NI', 'NL',
 
987
            'NO', 'NP', 'NR', 'NU', 'NZ', 'OM', 'PA', 'PE', 'PF', 'PG', 'PH',
 
988
            'PK', 'PL', 'PM', 'PN', 'PR', 'PS', 'PT', 'PW', 'PY', 'QA', 'RE',
 
989
            'RO', 'RS', 'RU', 'RW', 'SA', 'SB', 'SC', 'SD', 'SE', 'SG', 'SH',
 
990
            'SI', 'SJ', 'SK', 'SL', 'SM', 'SN', 'SO', 'SR', 'ST', 'SV', 'SY',
 
991
            'SZ', 'TC', 'TD', 'TF', 'TG', 'TH', 'TJ', 'TK', 'TL', 'TM', 'TN',
 
992
            'TO', 'TR', 'TT', 'TV', 'TW', 'TZ', 'UA', 'UG', 'UM', 'US', 'UY',
 
993
            'UZ', 'VA', 'VC', 'VE', 'VG', 'VI', 'VN', 'VU', 'WF', 'WS', 'YE',
 
994
            'YT', 'ZA', 'ZM', 'ZW')
 
995
        
881
996
    def type_to_url(self):
882
997
        return 'cht=t'
883
998
 
884
999
    def set_codes(self, codes):
885
 
        self.codes = codes
 
1000
        '''Set the country code map for the data.
 
1001
        Codes given in a list.
 
1002
 
 
1003
        i.e. DE - Germany
 
1004
             AT - Austria
 
1005
             US - United States
 
1006
        '''
 
1007
 
 
1008
        codemap = ''
 
1009
        
 
1010
        for cc in codes:
 
1011
            cc = cc.upper()
 
1012
            if cc in self.__ccodes:
 
1013
                codemap += cc
 
1014
            else:
 
1015
                raise UnknownCountryCodeException(cc)
 
1016
            
 
1017
        self.codes = codemap
 
1018
 
 
1019
    def set_geo_area(self, area):
 
1020
        '''Sets the geo area for the map.
 
1021
 
 
1022
        * africa
 
1023
        * asia
 
1024
        * europe
 
1025
        * middle_east
 
1026
        * south_america
 
1027
        * usa
 
1028
        * world
 
1029
        '''
 
1030
        
 
1031
        if area in self.__areas:
 
1032
            self.geo_area = area
 
1033
        else:
 
1034
            raise UnknownChartType('Unknown chart type for maps: %s' %area)
886
1035
 
887
1036
    def get_url_bits(self, data_class=None):
888
1037
        url_bits = Chart.get_url_bits(self, data_class=data_class)
891
1040
            url_bits.append('chld=%s' % ''.join(self.codes))
892
1041
        return url_bits
893
1042
 
 
1043
    def add_data_dict(self, datadict):
 
1044
        '''Sets the data and country codes via a dictionary.
 
1045
 
 
1046
        i.e. {'DE': 50, 'GB': 30, 'AT': 70}
 
1047
        '''
 
1048
 
 
1049
        self.set_codes(datadict.keys())
 
1050
        self.add_data(datadict.values())
 
1051
 
894
1052
 
895
1053
class GoogleOMeterChart(PieChart):
896
1054
    """Inheriting from PieChart because of similar labeling"""
897
1055
 
 
1056
    def __init__(self, *args, **kwargs):
 
1057
        PieChart.__init__(self, *args, **kwargs)
 
1058
        if self.auto_scale and not self.x_range:
 
1059
            warnings.warn('Please specify an x_range with GoogleOMeterChart, '
 
1060
                'otherwise one arrow will always be at the max.')
 
1061
 
898
1062
    def type_to_url(self):
899
1063
        return 'cht=gom'
900
1064
 
901
1065
 
902
 
def test():
903
 
    chart = PieChart2D(320, 200)
904
 
    chart = ScatterChart(320, 200)
905
 
    chart = SimpleLineChart(320, 200)
906
 
    chart = GroupedVerticalBarChart(320, 200)
907
 
#    chart = SplineRadarChart(500, 500)
908
 
#    chart = MapChart(440, 220)
909
 
#    chart = GoogleOMeterChart(440, 220, x_range=(0, 100))
910
 
    sine_data = [math.sin(float(a) / math.pi) * 100 for a in xrange(100)]
911
 
    random_data = [random.random() * 100 for a in xrange(100)]
912
 
    random_data2 = [random.random() * 50 for a in xrange(100)]
913
 
#    chart.set_bar_width(50)
914
 
#    chart.set_bar_spacing(0)
915
 
    chart.add_data(sine_data)
916
 
    chart.add_data(random_data)
917
 
    chart.set_zero_line(1, .5)
918
 
#    chart.add_data(random_data2)
919
 
#    chart.set_line_style(0, thickness=5)
920
 
#    chart.set_line_style(1, thickness=2, line_segment=10, blank_segment=5)
921
 
#    chart.set_title('heloooo weeee')
922
 
#    chart.set_legend(('sine wave', 'random * x'))
923
 
    chart.set_colours(('ee2000', 'DDDDAA', 'fF03f2'))
924
 
#    chart.fill_solid(Chart.ALPHA, '123456')
925
 
#    chart.fill_linear_gradient(Chart.ALPHA, 20, '004070', 1, '300040', 0,
926
 
#        'aabbcc55', 0.5)
927
 
#    chart.fill_linear_stripes(Chart.CHART, 20, '204070', .2, '300040', .2,
928
 
#        'aabbcc00', 0.2)
929
 
#    axis_left_index = chart.set_axis_range(Axis.LEFT, 0, 10)
930
 
#    axis_right_index = chart.set_axis_range(Axis.RIGHT, 5, 30)
931
 
#    axis_bottom_index = chart.set_axis_labels(Axis.BOTTOM, [1, 25, 95])
932
 
#    chart.set_axis_positions(axis_bottom_index, [1, 25, 95])
933
 
#    chart.set_axis_style(axis_bottom_index, '003050', 15)
934
 
 
935
 
#    chart.set_pie_labels(('apples', 'oranges', 'bananas'))
936
 
 
937
 
#    chart.set_grid(10, 10)
938
 
#    for a in xrange(0, 100, 10):
939
 
#        chart.add_marker(1, a, 'a', 'AACA20', 10)
940
 
 
941
 
#    chart.add_horizontal_range('00A020', .2, .5)
942
 
#    chart.add_vertical_range('00c030', .2, .4)
943
 
 
944
 
#    chart.add_fill_simple('303030A0')
945
 
 
946
 
#    chart.set_codes(['AU', 'AT', 'US'])
947
 
#    chart.add_data([1,2,3])
948
 
#    chart.set_colours(('EEEEEE', '000000', '00FF00'))
949
 
 
950
 
#    chart.add_data([50,75])
951
 
#    chart.set_pie_labels(('apples', 'oranges'))
952
 
 
953
 
    url = chart.get_url()
954
 
    print url
955
 
 
956
 
    chart.download('test.png')
957
 
 
958
 
    if 1:
959
 
        data = urllib.urlopen(chart.get_url()).read()
960
 
        open('meh.png', 'wb').write(data)
961
 
        os.system('eog meh.png')
962
 
 
963
 
 
964
 
if __name__ == '__main__':
965
 
    test()
 
1066
class QRChart(Chart):
 
1067
 
 
1068
    def __init__(self, *args, **kwargs):
 
1069
        Chart.__init__(self, *args, **kwargs)
 
1070
        self.encoding = None
 
1071
        self.ec_level = None
 
1072
        self.margin = None
 
1073
 
 
1074
    def type_to_url(self):
 
1075
        return 'cht=qr'
 
1076
 
 
1077
    def data_to_url(self, data_class=None):
 
1078
        if not self.data:
 
1079
            raise NoDataGivenException()
 
1080
        return 'chl=%s' % urllib.parse.quote(self.data[0])
 
1081
 
 
1082
    def get_url_bits(self, data_class=None):
 
1083
        url_bits = Chart.get_url_bits(self, data_class=data_class)
 
1084
        if self.encoding:
 
1085
            url_bits.append('choe=%s' % self.encoding)
 
1086
        if self.ec_level:
 
1087
            url_bits.append('chld=%s%%7c%s' % (self.ec_level, self.margin))
 
1088
        return url_bits
 
1089
 
 
1090
    def set_encoding(self, encoding):
 
1091
        self.encoding = encoding
 
1092
 
 
1093
    def set_ec(self, level, margin):
 
1094
        self.ec_level = level
 
1095
        self.margin = margin
 
1096
 
 
1097
 
 
1098
class ChartGrammar(object):
 
1099
 
 
1100
    def __init__(self):
 
1101
        self.grammar = None
 
1102
        self.chart = None
 
1103
 
 
1104
    def parse(self, grammar):
 
1105
        self.grammar = grammar
 
1106
        self.chart = self.create_chart_instance()
 
1107
 
 
1108
        for attr in self.grammar:
 
1109
            if attr in ('w', 'h', 'type', 'auto_scale', 'x_range', 'y_range'):
 
1110
                continue  # These are already parsed in create_chart_instance
 
1111
            attr_func = 'parse_' + attr
 
1112
            if not hasattr(self, attr_func):
 
1113
                warnings.warn('No parser for grammar attribute "%s"' % (attr))
 
1114
                continue
 
1115
            getattr(self, attr_func)(grammar[attr])
 
1116
 
 
1117
        return self.chart
 
1118
 
 
1119
    def parse_data(self, data):
 
1120
        self.chart.data = data
 
1121
 
 
1122
    @staticmethod
 
1123
    def get_possible_chart_types():
 
1124
        possible_charts = []
 
1125
        for cls_name in globals().keys():
 
1126
            if not cls_name.endswith('Chart'):
 
1127
                continue
 
1128
            cls = globals()[cls_name]
 
1129
            # Check if it is an abstract class
 
1130
            try:
 
1131
                a = cls(1, 1, auto_scale=False)
 
1132
                del a
 
1133
            except AbstractClassException:
 
1134
                continue
 
1135
            # Strip off "Class"
 
1136
            possible_charts.append(cls_name[:-5])
 
1137
        return possible_charts
 
1138
 
 
1139
    def create_chart_instance(self, grammar=None):
 
1140
        if not grammar:
 
1141
            grammar = self.grammar
 
1142
        assert(isinstance(grammar, dict))  # grammar must be a dict
 
1143
        assert('w' in grammar)  # width is required
 
1144
        assert('h' in grammar)  # height is required
 
1145
        assert('type' in grammar)  # type is required
 
1146
        chart_type = grammar['type']
 
1147
        w = grammar['w']
 
1148
        h = grammar['h']
 
1149
        auto_scale = grammar.get('auto_scale', None)
 
1150
        x_range = grammar.get('x_range', None)
 
1151
        y_range = grammar.get('y_range', None)
 
1152
        types = ChartGrammar.get_possible_chart_types()
 
1153
        if chart_type not in types:
 
1154
            raise UnknownChartType('%s is an unknown chart type. Possible '
 
1155
                'chart types are %s' % (chart_type, ','.join(types)))
 
1156
        return globals()[chart_type + 'Chart'](w, h, auto_scale=auto_scale,
 
1157
            x_range=x_range, y_range=y_range)
 
1158
 
 
1159
    def download(self):
 
1160
        pass
966
1161