/+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:
3
3
 
4
4
http://pygooglechart.slowchop.com/
5
5
 
6
 
Copyright 2007-2008 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.1'
 
36
__version__ = '0.3.0'
34
37
__author__ = 'Gerald Kaszuba'
35
38
 
36
39
reo_colour = re.compile('^([A-Fa-f0-9]{2,2}){3,4}$')
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
 
76
85
class UnknownChartType(PyGoogleChartException):
77
86
    pass
78
87
 
 
88
class UnknownCountryCodeException(PyGoogleChartException):
 
89
    pass
79
90
 
80
91
# Data Classes
81
92
# -----------------------------------------------------------------------------
92
103
    def float_scale_value(cls, value, range):
93
104
        lower, upper = range
94
105
        assert(upper > lower)
95
 
        max_value = cls.max_value()
96
 
        scaled = (value-lower) * (float(max_value) / (upper - lower))
 
106
        scaled = (value - lower) * (cls.max_value / (upper - lower))
97
107
        return scaled
98
108
 
99
109
    @classmethod
100
110
    def clip_value(cls, value):
101
 
        return max(0, min(value, cls.max_value()))
 
111
        return max(0, min(value, cls.max_value))
102
112
 
103
113
    @classmethod
104
114
    def int_scale_value(cls, value, range):
108
118
    def scale_value(cls, value, range):
109
119
        scaled = cls.int_scale_value(value, range)
110
120
        clipped = cls.clip_value(scaled)
 
121
        Data.check_clip(scaled, clipped)
111
122
        return clipped
112
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
 
113
130
 
114
131
class SimpleData(Data):
115
132
 
 
133
    max_value = 61
116
134
    enc_map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
117
135
 
118
136
    def __repr__(self):
119
 
        max_value = self.max_value()
120
137
        encoded_data = []
121
138
        for data in self.data:
122
139
            sub_data = []
123
140
            for value in data:
124
141
                if value is None:
125
142
                    sub_data.append('_')
126
 
                elif value >= 0 and value <= max_value:
 
143
                elif value >= 0 and value <= self.max_value:
127
144
                    sub_data.append(SimpleData.enc_map[value])
128
145
                else:
129
146
                    raise DataOutOfRangeException('cannot encode value: %d'
131
148
            encoded_data.append(''.join(sub_data))
132
149
        return 'chd=s:' + ','.join(encoded_data)
133
150
 
134
 
    @staticmethod
135
 
    def max_value():
136
 
        return 61
137
 
 
138
151
 
139
152
class TextData(Data):
140
153
 
 
154
    max_value = 100
 
155
 
141
156
    def __repr__(self):
142
 
        max_value = self.max_value()
143
157
        encoded_data = []
144
158
        for data in self.data:
145
159
            sub_data = []
146
160
            for value in data:
147
161
                if value is None:
148
162
                    sub_data.append(-1)
149
 
                elif value >= 0 and value <= max_value:
 
163
                elif value >= 0 and value <= self.max_value:
150
164
                    sub_data.append("%.1f" % float(value))
151
165
                else:
152
166
                    raise DataOutOfRangeException()
153
167
            encoded_data.append(','.join(sub_data))
154
 
        return 'chd=t:' + '|'.join(encoded_data)
155
 
 
156
 
    @staticmethod
157
 
    def max_value():
158
 
        return 100
 
168
        return 'chd=t:' + '%7c'.join(encoded_data)
159
169
 
160
170
    @classmethod
161
171
    def scale_value(cls, value, range):
163
173
        # map index
164
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
 
168
179
 
169
180
class ExtendedData(Data):
170
181
 
 
182
    max_value = 4095
171
183
    enc_map = \
172
184
        'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-.'
173
185
 
174
186
    def __repr__(self):
175
 
        max_value = self.max_value()
176
187
        encoded_data = []
177
188
        enc_size = len(ExtendedData.enc_map)
178
189
        for data in self.data:
180
191
            for value in data:
181
192
                if value is None:
182
193
                    sub_data.append('__')
183
 
                elif value >= 0 and value <= max_value:
 
194
                elif value >= 0 and value <= self.max_value:
184
195
                    first, second = divmod(int(value), enc_size)
185
196
                    sub_data.append('%s%s' % (
186
197
                        ExtendedData.enc_map[first],
192
203
            encoded_data.append(''.join(sub_data))
193
204
        return 'chd=e:' + ','.join(encoded_data)
194
205
 
195
 
    @staticmethod
196
 
    def max_value():
197
 
        return 4095
198
 
 
199
206
 
200
207
# Axis Classes
201
208
# -----------------------------------------------------------------------------
253
260
        self.values = [str(a) for a in values]
254
261
 
255
262
    def __repr__(self):
256
 
        return '%i:|%s' % (self.axis_index, '|'.join(self.values))
 
263
        return '%i:%%7c%s' % (self.axis_index, '%7c'.join(self.values))
257
264
 
258
265
 
259
266
class RangeAxis(Axis):
277
284
    of the chart. legend requires a list that corresponds to datasets.
278
285
    """
279
286
 
280
 
    BASE_URL = 'http://chart.apis.google.com/chart?'
 
287
    BASE_URL = 'http://chart.apis.google.com/chart'
281
288
    BACKGROUND = 'bg'
282
289
    CHART = 'c'
283
290
    ALPHA = 'a'
287
294
    LINEAR_STRIPES = 'ls'
288
295
 
289
296
    def __init__(self, width, height, title=None, legend=None, colours=None,
290
 
                 auto_scale=True, x_range=None, y_range=None):
 
297
            auto_scale=True, x_range=None, y_range=None,
 
298
            colours_within_series=None):
291
299
        if type(self) == Chart:
292
300
            raise AbstractClassException('This is an abstract class')
293
301
        assert(isinstance(width, int))
296
304
        self.height = height
297
305
        self.data = []
298
306
        self.set_title(title)
 
307
        self.set_title_style(None, None)
299
308
        self.set_legend(legend)
 
309
        self.set_legend_position(None)
300
310
        self.set_colours(colours)
 
311
        self.set_colours_within_series(colours_within_series)
301
312
 
302
313
        # Data for scaling.
303
 
        self.auto_scale = auto_scale    # Whether to automatically scale data
304
 
        self.x_range = x_range          # (min, max) x-axis range for scaling
305
 
        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
306
317
        self.scaled_data_class = None
307
318
        self.scaled_x_range = None
308
319
        self.scaled_y_range = None
321
332
        self.markers = []
322
333
        self.line_styles = {}
323
334
        self.grid = None
 
335
        self.title_colour = None
 
336
        self.title_font_size = None
324
337
 
325
338
    # URL generation
326
339
    # -------------------------------------------------------------------------
327
 
 
 
340
        
328
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):
329
345
        url_bits = self.get_url_bits(data_class=data_class)
330
 
        return self.BASE_URL + '&'.join(url_bits)
 
346
        return '&'.join(url_bits)
331
347
 
332
348
    def get_url_bits(self, data_class=None):
333
349
        url_bits = []
338
354
        # optional arguments
339
355
        if self.title:
340
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))
341
360
        if self.legend:
342
 
            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))
343
364
        if self.colours:
344
 
            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))
345
368
        ret = self.fill_to_url()
346
369
        if ret:
347
370
            url_bits.append(ret)
348
371
        ret = self.axis_to_url()
349
372
        if ret:
350
 
            url_bits.append(ret)
 
373
            url_bits.append(ret)                    
351
374
        if self.markers:
352
 
            url_bits.append(self.markers_to_url())
 
375
            url_bits.append(self.markers_to_url())        
353
376
        if self.line_styles:
354
377
            style = []
355
 
            for index in xrange(max(self.line_styles) + 1):
 
378
            for index in range(max(self.line_styles) + 1):
356
379
                if index in self.line_styles:
357
380
                    values = self.line_styles[index]
358
381
                else:
359
382
                    values = ('1', )
360
383
                style.append(','.join(values))
361
 
            url_bits.append('chls=%s' % '|'.join(style))
 
384
            url_bits.append('chls=%s' % '%7c'.join(style))
362
385
        if self.grid:
363
386
            url_bits.append('chg=%s' % self.grid)
364
387
        return url_bits
366
389
    # Downloading
367
390
    # -------------------------------------------------------------------------
368
391
 
369
 
    def download(self, file_name):
370
 
        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())
371
397
 
372
398
        if opener.headers['content-type'] != 'image/png':
373
399
            raise BadContentTypeException('Server responded with a ' \
374
400
                'content-type of %s' % opener.headers['content-type'])
375
401
 
376
 
        open(file_name, 'wb').write(urllib.urlopen(self.get_url()).read())
 
402
        open(file_name, 'wb').write(opener.read())
377
403
 
378
404
    # Simple settings
379
405
    # -------------------------------------------------------------------------
380
406
 
381
407
    def set_title(self, title):
382
408
        if title:
383
 
            self.title = urllib.quote(title)
 
409
            self.title = urllib.parse.quote(title)
384
410
        else:
385
411
            self.title = None
386
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
 
387
421
    def set_legend(self, legend):
388
422
        """legend needs to be a list, tuple or None"""
389
423
        assert(isinstance(legend, list) or isinstance(legend, tuple) or
390
424
            legend is None)
391
425
        if legend:
392
 
            self.legend = [urllib.quote(a) for a in legend]
 
426
            self.legend = [urllib.parse.quote(a) for a in legend]
393
427
        else:
394
428
            self.legend = None
395
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
 
396
436
    # Chart colours
397
437
    # -------------------------------------------------------------------------
398
438
 
406
446
                _check_colour(col)
407
447
        self.colours = colours
408
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
 
409
459
    # Background/Chart colours
410
460
    # -------------------------------------------------------------------------
411
461
 
420
470
        assert(angle >= 0 and angle <= 90)
421
471
        assert(len(args) % 2 == 0)
422
472
        args = list(args)  # args is probably a tuple and we need to mutate
423
 
        for a in xrange(len(args) / 2):
 
473
        for a in range(int(len(args) / 2)):
424
474
            col = args[a * 2]
425
475
            offset = args[a * 2 + 1]
426
476
            _check_colour(col)
447
497
                areas.append('%s,%s,%s' % (area, self.fill_types[area], \
448
498
                    self.fill_area[area]))
449
499
        if areas:
450
 
            return 'chf=' + '|'.join(areas)
 
500
            return 'chf=' + '%7c'.join(areas)
451
501
 
452
502
    # Data
453
503
    # -------------------------------------------------------------------------
471
521
        else:
472
522
            return ExtendedData
473
523
 
 
524
    def _filter_none(self, data):
 
525
        return [r for r in data if r is not None]
 
526
 
474
527
    def data_x_range(self):
475
528
        """Return a 2-tuple giving the minimum and maximum x-axis
476
529
        data range.
477
530
        """
478
531
        try:
479
 
            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()
480
534
                         if type == 'x'])
481
 
            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()
482
537
                         if type == 'x'])
483
538
            return (lower, upper)
484
539
        except ValueError:
489
544
        data range.
490
545
        """
491
546
        try:
492
 
            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()
493
549
                         if type == 'y'])
494
 
            upper = max([max(s) + 1 for type, s in self.annotated_data()
 
550
            upper = max([max(self._filter_none(s)) + 1
 
551
                         for type, s in self.annotated_data()
495
552
                         if type == 'y'])
496
553
            return (lower, upper)
497
554
        except ValueError:
517
574
        if x_range is None:
518
575
            x_range = self.data_x_range()
519
576
            if x_range and x_range[0] > 0:
520
 
                x_range = (0, x_range[1])
 
577
                x_range = (x_range[0], x_range[1])
521
578
        self.scaled_x_range = x_range
522
579
 
523
580
        # Determine the y-axis range for scaling.
524
581
        if y_range is None:
525
582
            y_range = self.data_y_range()
526
583
            if y_range and y_range[0] > 0:
527
 
                y_range = (0, y_range[1])
 
584
                y_range = (y_range[0], y_range[1])
528
585
        self.scaled_y_range = y_range
529
586
 
530
587
        scaled_data = []
535
592
                scale_range = y_range
536
593
            elif type == 'marker-size':
537
594
                scale_range = (0, max(dataset))
538
 
            scaled_data.append([data_class.scale_value(v, scale_range)
539
 
                                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)
540
603
        return scaled_data
541
604
 
542
605
    def add_data(self, data):
549
612
        if not issubclass(data_class, Data):
550
613
            raise UnknownDataTypeException()
551
614
        if self.auto_scale:
552
 
            print data_class
553
615
            data = self.scaled_data(data_class, self.x_range, self.y_range)
554
616
        else:
555
617
            data = self.data
564
626
 
565
627
    def set_axis_labels(self, axis_type, values):
566
628
        assert(axis_type in Axis.TYPES)
567
 
        values = [urllib.quote(a) for a in values]
 
629
        values = [urllib.parse.quote(str(a)) for a in values]
568
630
        axis_index = len(self.axis)
569
631
        axis = LabelAxis(axis_index, axis_type, values)
570
632
        self.axis.append(axis)
614
676
        url_bits = []
615
677
        url_bits.append('chxt=%s' % ','.join(available_axis))
616
678
        if label_axis:
617
 
            url_bits.append('chxl=%s' % '|'.join(label_axis))
 
679
            url_bits.append('chxl=%s' % '%7c'.join(label_axis))
618
680
        if range_axis:
619
 
            url_bits.append('chxr=%s' % '|'.join(range_axis))
 
681
            url_bits.append('chxr=%s' % '%7c'.join(range_axis))
620
682
        if positions:
621
 
            url_bits.append('chxp=%s' % '|'.join(positions))
 
683
            url_bits.append('chxp=%s' % '%7c'.join(positions))
622
684
        if styles:
623
 
            url_bits.append('chxs=%s' % '|'.join(styles))
 
685
            url_bits.append('chxs=%s' % '%7c'.join(styles))
624
686
        return '&'.join(url_bits)
625
687
 
626
688
    # Markers, Ranges and Fill area (chm)
627
689
    # -------------------------------------------------------------------------
628
690
 
629
 
    def markers_to_url(self):
630
 
        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])
631
693
 
632
694
    def add_marker(self, index, point, marker_type, colour, size, priority=0):
633
695
        self.markers.append((marker_type, colour, str(index), str(point), \
634
696
            str(size), str(priority)))
635
697
 
636
698
    def add_horizontal_range(self, colour, start, stop):
637
 
        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)))        
638
709
 
639
710
    def add_vertical_range(self, colour, start, stop):
640
 
        self.markers.append(('R', colour, '1', str(start), str(stop)))
 
711
        self.markers.append(('R', colour, '0', str(start), str(stop)))
641
712
 
642
713
    def add_fill_range(self, colour, index_start, index_end):
643
714
        self.markers.append(('b', colour, str(index_start), str(index_end), \
741
812
            url_bits.append('chbh=%i' % self.bar_width)
742
813
        zero_line = []
743
814
        if self.zero_lines:
744
 
            for index in xrange(max(self.zero_lines) + 1):
 
815
            for index in range(max(self.zero_lines) + 1):
745
816
                if index in self.zero_lines:
746
817
                    zero_line.append(str(self.zero_lines[index]))
747
818
                else:
830
901
            raise AbstractClassException('This is an abstract class')
831
902
        Chart.__init__(self, *args, **kwargs)
832
903
        self.pie_labels = []
 
904
        if self.y_range:
 
905
            warnings.warn('y_range is not used with %s.' % \
 
906
                (self.__class__.__name__))
833
907
 
834
908
    def set_pie_labels(self, labels):
835
 
        self.pie_labels = [urllib.quote(a) for a in labels]
 
909
        self.pie_labels = [urllib.parse.quote(a) for a in labels]
836
910
 
837
911
    def get_url_bits(self, data_class=None):
838
912
        url_bits = Chart.get_url_bits(self, data_class=data_class)
839
913
        if self.pie_labels:
840
 
            url_bits.append('chl=%s' % '|'.join(self.pie_labels))
 
914
            url_bits.append('chl=%s' % '%7c'.join(self.pie_labels))
841
915
        return url_bits
842
916
 
843
917
    def annotated_data(self):
844
918
        # Datasets are all y-axis data. However, there should only be
845
919
        # one dataset for pie charts.
846
920
        for dataset in self.data:
847
 
            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)
848
927
 
849
928
 
850
929
class PieChart2D(PieChart):
887
966
        Chart.__init__(self, *args, **kwargs)
888
967
        self.geo_area = 'world'
889
968
        self.codes = []
890
 
 
 
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
        
891
996
    def type_to_url(self):
892
997
        return 'cht=t'
893
998
 
894
999
    def set_codes(self, codes):
895
 
        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)
896
1035
 
897
1036
    def get_url_bits(self, data_class=None):
898
1037
        url_bits = Chart.get_url_bits(self, data_class=data_class)
901
1040
            url_bits.append('chld=%s' % ''.join(self.codes))
902
1041
        return url_bits
903
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
 
904
1052
 
905
1053
class GoogleOMeterChart(PieChart):
906
1054
    """Inheriting from PieChart because of similar labeling"""
907
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
 
908
1062
    def type_to_url(self):
909
1063
        return 'cht=gom'
910
1064
 
911
1065
 
 
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
 
912
1098
class ChartGrammar(object):
913
1099
 
914
 
    def __init__(self, grammar):
 
1100
    def __init__(self):
 
1101
        self.grammar = None
 
1102
        self.chart = None
 
1103
 
 
1104
    def parse(self, grammar):
915
1105
        self.grammar = grammar
916
1106
        self.chart = self.create_chart_instance()
917
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
 
918
1122
    @staticmethod
919
1123
    def get_possible_chart_types():
920
1124
        possible_charts = []
921
 
        for cls_name in globals():
 
1125
        for cls_name in globals().keys():
922
1126
            if not cls_name.endswith('Chart'):
923
1127
                continue
924
1128
            cls = globals()[cls_name]
925
1129
            # Check if it is an abstract class
926
1130
            try:
927
 
                cls(1, 1)
 
1131
                a = cls(1, 1, auto_scale=False)
 
1132
                del a
928
1133
            except AbstractClassException:
929
1134
                continue
930
1135
            # Strip off "Class"
931
1136
            possible_charts.append(cls_name[:-5])
932
1137
        return possible_charts
933
1138
 
934
 
    def create_chart_instance(self):
 
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
935
1143
        assert('w' in grammar)  # width is required
936
1144
        assert('h' in grammar)  # height is required
937
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)
938
1152
        types = ChartGrammar.get_possible_chart_types()
939
 
        if grammar['type'] not in types:
 
1153
        if chart_type not in types:
940
1154
            raise UnknownChartType('%s is an unknown chart type. Possible '
941
 
                'chart types are %s' % (grammar['type'], ','.join(types)))
 
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)
942
1158
 
943
1159
    def download(self):
944
1160
        pass