/+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: gak
  • Date: 2008-05-03 04:23:51 UTC
  • Revision ID: git-v1:1f4e14367f685fad4e70b381063c5ca195704615
- Initial "grammar" code
- New exception types: AbstractClassException and UnknownChartType
- More unit tests
- Removed tests from within pygooglechart.py

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