/+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: 2009-03-15 09:26:16 UTC
  • Revision ID: git-v1:4b470bcc49f53e8203d87d08be532215701881a6
 - #16 Allow either arguments in set_title_style()

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
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):
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
    # -------------------------------------------------------------------------
338
351
        # optional arguments
339
352
        if self.title:
340
353
            url_bits.append('chtt=%s' % self.title)
 
354
        if self.title_colour and self.title_font_size:
 
355
            url_bits.append('chts=%s,%s' % (self.title_colour, \
 
356
                self.title_font_size))
341
357
        if self.legend:
342
 
            url_bits.append('chdl=%s' % '|'.join(self.legend))
 
358
            url_bits.append('chdl=%s' % '%7c'.join(self.legend))
 
359
        if self.legend_position:
 
360
            url_bits.append('chdlp=%s' % (self.legend_position))
343
361
        if self.colours:
344
 
            url_bits.append('chco=%s' % ','.join(self.colours))
 
362
            url_bits.append('chco=%s' % ','.join(self.colours))            
 
363
        if self.colours_within_series:
 
364
            url_bits.append('chco=%s' % '%7c'.join(self.colours_within_series))
345
365
        ret = self.fill_to_url()
346
366
        if ret:
347
367
            url_bits.append(ret)
348
368
        ret = self.axis_to_url()
349
369
        if ret:
350
 
            url_bits.append(ret)
 
370
            url_bits.append(ret)                    
351
371
        if self.markers:
352
 
            url_bits.append(self.markers_to_url())
 
372
            url_bits.append(self.markers_to_url())        
353
373
        if self.line_styles:
354
374
            style = []
355
375
            for index in xrange(max(self.line_styles) + 1):
358
378
                else:
359
379
                    values = ('1', )
360
380
                style.append(','.join(values))
361
 
            url_bits.append('chls=%s' % '|'.join(style))
 
381
            url_bits.append('chls=%s' % '%7c'.join(style))
362
382
        if self.grid:
363
383
            url_bits.append('chg=%s' % self.grid)
364
384
        return url_bits
373
393
            raise BadContentTypeException('Server responded with a ' \
374
394
                'content-type of %s' % opener.headers['content-type'])
375
395
 
376
 
        open(file_name, 'wb').write(urllib.urlopen(self.get_url()).read())
 
396
        open(file_name, 'wb').write(opener.read())
377
397
 
378
398
    # Simple settings
379
399
    # -------------------------------------------------------------------------
384
404
        else:
385
405
            self.title = None
386
406
 
 
407
    def set_title_style(self, colour=None, font_size=None):
 
408
        if not colour is None:
 
409
            _check_colour(colour)
 
410
        if not colour and not font_size:
 
411
            return
 
412
        self.title_colour = colour or '333333'
 
413
        self.title_font_size = font_size or 13.5
 
414
 
387
415
    def set_legend(self, legend):
388
416
        """legend needs to be a list, tuple or None"""
389
417
        assert(isinstance(legend, list) or isinstance(legend, tuple) or
393
421
        else:
394
422
            self.legend = None
395
423
 
 
424
    def set_legend_position(self, legend_position):
 
425
        if legend_position:
 
426
            self.legend_position = urllib.quote(legend_position)
 
427
        else:    
 
428
            self.legend_position = None
 
429
 
396
430
    # Chart colours
397
431
    # -------------------------------------------------------------------------
398
432
 
406
440
                _check_colour(col)
407
441
        self.colours = colours
408
442
 
 
443
    def set_colours_within_series(self, colours):
 
444
        # colours needs to be a list, tuple or None
 
445
        assert(isinstance(colours, list) or isinstance(colours, tuple) or
 
446
            colours is None)
 
447
        # make sure the colours are in the right format
 
448
        if colours:
 
449
            for col in colours:
 
450
                _check_colour(col)
 
451
        self.colours_within_series = colours        
 
452
 
409
453
    # Background/Chart colours
410
454
    # -------------------------------------------------------------------------
411
455
 
420
464
        assert(angle >= 0 and angle <= 90)
421
465
        assert(len(args) % 2 == 0)
422
466
        args = list(args)  # args is probably a tuple and we need to mutate
423
 
        for a in xrange(len(args) / 2):
 
467
        for a in xrange(int(len(args) / 2)):
424
468
            col = args[a * 2]
425
469
            offset = args[a * 2 + 1]
426
470
            _check_colour(col)
447
491
                areas.append('%s,%s,%s' % (area, self.fill_types[area], \
448
492
                    self.fill_area[area]))
449
493
        if areas:
450
 
            return 'chf=' + '|'.join(areas)
 
494
            return 'chf=' + '%7c'.join(areas)
451
495
 
452
496
    # Data
453
497
    # -------------------------------------------------------------------------
471
515
        else:
472
516
            return ExtendedData
473
517
 
 
518
    def _filter_none(self, data):
 
519
        return [r for r in data if r is not None]
 
520
 
474
521
    def data_x_range(self):
475
522
        """Return a 2-tuple giving the minimum and maximum x-axis
476
523
        data range.
477
524
        """
478
525
        try:
479
 
            lower = min([min(s) for type, s in self.annotated_data()
 
526
            lower = min([min(self._filter_none(s))
 
527
                         for type, s in self.annotated_data()
480
528
                         if type == 'x'])
481
 
            upper = max([max(s) for type, s in self.annotated_data()
 
529
            upper = max([max(self._filter_none(s))
 
530
                         for type, s in self.annotated_data()
482
531
                         if type == 'x'])
483
532
            return (lower, upper)
484
533
        except ValueError:
489
538
        data range.
490
539
        """
491
540
        try:
492
 
            lower = min([min(s) for type, s in self.annotated_data()
 
541
            lower = min([min(self._filter_none(s))
 
542
                         for type, s in self.annotated_data()
493
543
                         if type == 'y'])
494
 
            upper = max([max(s) + 1 for type, s in self.annotated_data()
 
544
            upper = max([max(self._filter_none(s)) + 1
 
545
                         for type, s in self.annotated_data()
495
546
                         if type == 'y'])
496
547
            return (lower, upper)
497
548
        except ValueError:
517
568
        if x_range is None:
518
569
            x_range = self.data_x_range()
519
570
            if x_range and x_range[0] > 0:
520
 
                x_range = (0, x_range[1])
 
571
                x_range = (x_range[0], x_range[1])
521
572
        self.scaled_x_range = x_range
522
573
 
523
574
        # Determine the y-axis range for scaling.
524
575
        if y_range is None:
525
576
            y_range = self.data_y_range()
526
577
            if y_range and y_range[0] > 0:
527
 
                y_range = (0, y_range[1])
 
578
                y_range = (y_range[0], y_range[1])
528
579
        self.scaled_y_range = y_range
529
580
 
530
581
        scaled_data = []
535
586
                scale_range = y_range
536
587
            elif type == 'marker-size':
537
588
                scale_range = (0, max(dataset))
538
 
            scaled_data.append([data_class.scale_value(v, scale_range)
539
 
                                for v in dataset])
 
589
            scaled_dataset = []
 
590
            for v in dataset:
 
591
                if v is None:
 
592
                    scaled_dataset.append(None)
 
593
                else:
 
594
                    scaled_dataset.append(
 
595
                        data_class.scale_value(v, scale_range))
 
596
            scaled_data.append(scaled_dataset)
540
597
        return scaled_data
541
598
 
542
599
    def add_data(self, data):
549
606
        if not issubclass(data_class, Data):
550
607
            raise UnknownDataTypeException()
551
608
        if self.auto_scale:
552
 
            print data_class
553
609
            data = self.scaled_data(data_class, self.x_range, self.y_range)
554
610
        else:
555
611
            data = self.data
564
620
 
565
621
    def set_axis_labels(self, axis_type, values):
566
622
        assert(axis_type in Axis.TYPES)
567
 
        values = [urllib.quote(a) for a in values]
 
623
        values = [urllib.quote(str(a)) for a in values]
568
624
        axis_index = len(self.axis)
569
625
        axis = LabelAxis(axis_index, axis_type, values)
570
626
        self.axis.append(axis)
614
670
        url_bits = []
615
671
        url_bits.append('chxt=%s' % ','.join(available_axis))
616
672
        if label_axis:
617
 
            url_bits.append('chxl=%s' % '|'.join(label_axis))
 
673
            url_bits.append('chxl=%s' % '%7c'.join(label_axis))
618
674
        if range_axis:
619
 
            url_bits.append('chxr=%s' % '|'.join(range_axis))
 
675
            url_bits.append('chxr=%s' % '%7c'.join(range_axis))
620
676
        if positions:
621
 
            url_bits.append('chxp=%s' % '|'.join(positions))
 
677
            url_bits.append('chxp=%s' % '%7c'.join(positions))
622
678
        if styles:
623
 
            url_bits.append('chxs=%s' % '|'.join(styles))
 
679
            url_bits.append('chxs=%s' % '%7c'.join(styles))
624
680
        return '&'.join(url_bits)
625
681
 
626
682
    # Markers, Ranges and Fill area (chm)
627
683
    # -------------------------------------------------------------------------
628
684
 
629
 
    def markers_to_url(self):
630
 
        return 'chm=%s' % '|'.join([','.join(a) for a in self.markers])
 
685
    def markers_to_url(self):        
 
686
        return 'chm=%s' % '%7c'.join([','.join(a) for a in self.markers])
631
687
 
632
688
    def add_marker(self, index, point, marker_type, colour, size, priority=0):
633
689
        self.markers.append((marker_type, colour, str(index), str(point), \
634
690
            str(size), str(priority)))
635
691
 
636
692
    def add_horizontal_range(self, colour, start, stop):
637
 
        self.markers.append(('r', colour, '1', str(start), str(stop)))
 
693
        self.markers.append(('r', colour, '0', str(start), str(stop)))
 
694
 
 
695
    def add_data_line(self, colour, data_set, size, priority=0):
 
696
        self.markers.append(('D', colour, str(data_set), '0', str(size), \
 
697
            str(priority)))
 
698
 
 
699
    def add_marker_text(self, string, colour, data_set, data_point, size, \
 
700
            priority=0):
 
701
        self.markers.append((str(string), colour, str(data_set), \
 
702
            str(data_point), str(size), str(priority)))        
638
703
 
639
704
    def add_vertical_range(self, colour, start, stop):
640
 
        self.markers.append(('R', colour, '1', str(start), str(stop)))
 
705
        self.markers.append(('R', colour, '0', str(start), str(stop)))
641
706
 
642
707
    def add_fill_range(self, colour, index_start, index_end):
643
708
        self.markers.append(('b', colour, str(index_start), str(index_end), \
830
895
            raise AbstractClassException('This is an abstract class')
831
896
        Chart.__init__(self, *args, **kwargs)
832
897
        self.pie_labels = []
 
898
        if self.y_range:
 
899
            warnings.warn('y_range is not used with %s.' % \
 
900
                (self.__class__.__name__))
833
901
 
834
902
    def set_pie_labels(self, labels):
835
903
        self.pie_labels = [urllib.quote(a) for a in labels]
837
905
    def get_url_bits(self, data_class=None):
838
906
        url_bits = Chart.get_url_bits(self, data_class=data_class)
839
907
        if self.pie_labels:
840
 
            url_bits.append('chl=%s' % '|'.join(self.pie_labels))
 
908
            url_bits.append('chl=%s' % '%7c'.join(self.pie_labels))
841
909
        return url_bits
842
910
 
843
911
    def annotated_data(self):
844
912
        # Datasets are all y-axis data. However, there should only be
845
913
        # one dataset for pie charts.
846
914
        for dataset in self.data:
847
 
            yield ('y', dataset)
 
915
            yield ('x', dataset)
 
916
 
 
917
    def scaled_data(self, data_class, x_range=None, y_range=None):
 
918
        if not x_range:
 
919
            x_range = [0, sum(self.data[0])]
 
920
        return Chart.scaled_data(self, data_class, x_range, self.y_range)
848
921
 
849
922
 
850
923
class PieChart2D(PieChart):
887
960
        Chart.__init__(self, *args, **kwargs)
888
961
        self.geo_area = 'world'
889
962
        self.codes = []
890
 
 
 
963
        self.__areas = ('africa', 'asia', 'europe', 'middle_east',
 
964
            'south_america', 'usa', 'world')
 
965
        self.__ccodes = (
 
966
            'AD', 'AE', 'AF', 'AG', 'AI', 'AL', 'AM', 'AN', 'AO', 'AQ', 'AR',
 
967
            'AS', 'AT', 'AU', 'AW', 'AX', 'AZ', 'BA', 'BB', 'BD', 'BE', 'BF',
 
968
            'BG', 'BH', 'BI', 'BJ', 'BL', 'BM', 'BN', 'BO', 'BR', 'BS', 'BT',
 
969
            'BV', 'BW', 'BY', 'BZ', 'CA', 'CC', 'CD', 'CF', 'CG', 'CH', 'CI',
 
970
            'CK', 'CL', 'CM', 'CN', 'CO', 'CR', 'CU', 'CV', 'CX', 'CY', 'CZ',
 
971
            'DE', 'DJ', 'DK', 'DM', 'DO', 'DZ', 'EC', 'EE', 'EG', 'EH', 'ER',
 
972
            'ES', 'ET', 'FI', 'FJ', 'FK', 'FM', 'FO', 'FR', 'GA', 'GB', 'GD',
 
973
            'GE', 'GF', 'GG', 'GH', 'GI', 'GL', 'GM', 'GN', 'GP', 'GQ', 'GR',
 
974
            'GS', 'GT', 'GU', 'GW', 'GY', 'HK', 'HM', 'HN', 'HR', 'HT', 'HU',
 
975
            'ID', 'IE', 'IL', 'IM', 'IN', 'IO', 'IQ', 'IR', 'IS', 'IT', 'JE',
 
976
            'JM', 'JO', 'JP', 'KE', 'KG', 'KH', 'KI', 'KM', 'KN', 'KP', 'KR',
 
977
            'KW', 'KY', 'KZ', 'LA', 'LB', 'LC', 'LI', 'LK', 'LR', 'LS', 'LT',
 
978
            'LU', 'LV', 'LY', 'MA', 'MC', 'MD', 'ME', 'MF', 'MG', 'MH', 'MK',
 
979
            'ML', 'MM', 'MN', 'MO', 'MP', 'MQ', 'MR', 'MS', 'MT', 'MU', 'MV',
 
980
            'MW', 'MX', 'MY', 'MZ', 'NA', 'NC', 'NE', 'NF', 'NG', 'NI', 'NL',
 
981
            'NO', 'NP', 'NR', 'NU', 'NZ', 'OM', 'PA', 'PE', 'PF', 'PG', 'PH',
 
982
            'PK', 'PL', 'PM', 'PN', 'PR', 'PS', 'PT', 'PW', 'PY', 'QA', 'RE',
 
983
            'RO', 'RS', 'RU', 'RW', 'SA', 'SB', 'SC', 'SD', 'SE', 'SG', 'SH',
 
984
            'SI', 'SJ', 'SK', 'SL', 'SM', 'SN', 'SO', 'SR', 'ST', 'SV', 'SY',
 
985
            'SZ', 'TC', 'TD', 'TF', 'TG', 'TH', 'TJ', 'TK', 'TL', 'TM', 'TN',
 
986
            'TO', 'TR', 'TT', 'TV', 'TW', 'TZ', 'UA', 'UG', 'UM', 'US', 'UY',
 
987
            'UZ', 'VA', 'VC', 'VE', 'VG', 'VI', 'VN', 'VU', 'WF', 'WS', 'YE',
 
988
            'YT', 'ZA', 'ZM', 'ZW')
 
989
        
891
990
    def type_to_url(self):
892
991
        return 'cht=t'
893
992
 
894
993
    def set_codes(self, codes):
895
 
        self.codes = codes
 
994
        '''Set the country code map for the data.
 
995
        Codes given in a list.
 
996
 
 
997
        i.e. DE - Germany
 
998
             AT - Austria
 
999
             US - United States
 
1000
        '''
 
1001
 
 
1002
        codemap = ''
 
1003
        
 
1004
        for cc in codes:
 
1005
            cc = cc.upper()
 
1006
            if cc in self.__ccodes:
 
1007
                codemap += cc
 
1008
            else:
 
1009
                raise UnknownCountryCodeException(cc)
 
1010
            
 
1011
        self.codes = codemap
 
1012
 
 
1013
    def set_geo_area(self, area):
 
1014
        '''Sets the geo area for the map.
 
1015
 
 
1016
        * africa
 
1017
        * asia
 
1018
        * europe
 
1019
        * middle_east
 
1020
        * south_america
 
1021
        * usa
 
1022
        * world
 
1023
        '''
 
1024
        
 
1025
        if area in self.__areas:
 
1026
            self.geo_area = area
 
1027
        else:
 
1028
            raise UnknownChartType('Unknown chart type for maps: %s' %area)
896
1029
 
897
1030
    def get_url_bits(self, data_class=None):
898
1031
        url_bits = Chart.get_url_bits(self, data_class=data_class)
901
1034
            url_bits.append('chld=%s' % ''.join(self.codes))
902
1035
        return url_bits
903
1036
 
 
1037
    def add_data_dict(self, datadict):
 
1038
        '''Sets the data and country codes via a dictionary.
 
1039
 
 
1040
        i.e. {'DE': 50, 'GB': 30, 'AT': 70}
 
1041
        '''
 
1042
 
 
1043
        self.set_codes(datadict.keys())
 
1044
        self.add_data(datadict.values())
 
1045
 
904
1046
 
905
1047
class GoogleOMeterChart(PieChart):
906
1048
    """Inheriting from PieChart because of similar labeling"""
907
1049
 
 
1050
    def __init__(self, *args, **kwargs):
 
1051
        PieChart.__init__(self, *args, **kwargs)
 
1052
        if self.auto_scale and not self.x_range:
 
1053
            warnings.warn('Please specify an x_range with GoogleOMeterChart, '
 
1054
                'otherwise one arrow will always be at the max.')
 
1055
 
908
1056
    def type_to_url(self):
909
1057
        return 'cht=gom'
910
1058
 
911
1059
 
 
1060
class QRChart(Chart):
 
1061
 
 
1062
    def __init__(self, *args, **kwargs):
 
1063
        Chart.__init__(self, *args, **kwargs)
 
1064
        self.encoding = None
 
1065
        self.ec_level = None
 
1066
        self.margin = None
 
1067
 
 
1068
    def type_to_url(self):
 
1069
        return 'cht=qr'
 
1070
 
 
1071
    def data_to_url(self, data_class=None):
 
1072
        if not self.data:
 
1073
            raise NoDataGivenException()
 
1074
        return 'chl=%s' % urllib.quote(self.data[0])
 
1075
 
 
1076
    def get_url_bits(self, data_class=None):
 
1077
        url_bits = Chart.get_url_bits(self, data_class=data_class)
 
1078
        if self.encoding:
 
1079
            url_bits.append('choe=%s' % self.encoding)
 
1080
        if self.ec_level:
 
1081
            url_bits.append('chld=%s%%7c%s' % (self.ec_level, self.margin))
 
1082
        return url_bits
 
1083
 
 
1084
    def set_encoding(self, encoding):
 
1085
        self.encoding = encoding
 
1086
 
 
1087
    def set_ec(self, level, margin):
 
1088
        self.ec_level = level
 
1089
        self.margin = margin
 
1090
 
 
1091
 
912
1092
class ChartGrammar(object):
913
1093
 
914
 
    def __init__(self, grammar):
 
1094
    def __init__(self):
 
1095
        self.grammar = None
 
1096
        self.chart = None
 
1097
 
 
1098
    def parse(self, grammar):
915
1099
        self.grammar = grammar
916
1100
        self.chart = self.create_chart_instance()
917
1101
 
 
1102
        for attr in self.grammar:
 
1103
            if attr in ('w', 'h', 'type', 'auto_scale', 'x_range', 'y_range'):
 
1104
                continue  # These are already parsed in create_chart_instance
 
1105
            attr_func = 'parse_' + attr
 
1106
            if not hasattr(self, attr_func):
 
1107
                warnings.warn('No parser for grammar attribute "%s"' % (attr))
 
1108
                continue
 
1109
            getattr(self, attr_func)(grammar[attr])
 
1110
 
 
1111
        return self.chart
 
1112
 
 
1113
    def parse_data(self, data):
 
1114
        self.chart.data = data
 
1115
 
918
1116
    @staticmethod
919
1117
    def get_possible_chart_types():
920
1118
        possible_charts = []
921
 
        for cls_name in globals():
 
1119
        for cls_name in globals().keys():
922
1120
            if not cls_name.endswith('Chart'):
923
1121
                continue
924
1122
            cls = globals()[cls_name]
925
1123
            # Check if it is an abstract class
926
1124
            try:
927
 
                cls(1, 1)
 
1125
                a = cls(1, 1, auto_scale=False)
 
1126
                del a
928
1127
            except AbstractClassException:
929
1128
                continue
930
1129
            # Strip off "Class"
931
1130
            possible_charts.append(cls_name[:-5])
932
1131
        return possible_charts
933
1132
 
934
 
    def create_chart_instance(self):
 
1133
    def create_chart_instance(self, grammar=None):
 
1134
        if not grammar:
 
1135
            grammar = self.grammar
 
1136
        assert(isinstance(grammar, dict))  # grammar must be a dict
935
1137
        assert('w' in grammar)  # width is required
936
1138
        assert('h' in grammar)  # height is required
937
1139
        assert('type' in grammar)  # type is required
 
1140
        chart_type = grammar['type']
 
1141
        w = grammar['w']
 
1142
        h = grammar['h']
 
1143
        auto_scale = grammar.get('auto_scale', None)
 
1144
        x_range = grammar.get('x_range', None)
 
1145
        y_range = grammar.get('y_range', None)
938
1146
        types = ChartGrammar.get_possible_chart_types()
939
 
        if grammar['type'] not in types:
 
1147
        if chart_type not in types:
940
1148
            raise UnknownChartType('%s is an unknown chart type. Possible '
941
 
                'chart types are %s' % (grammar['type'], ','.join(types)))
 
1149
                'chart types are %s' % (chart_type, ','.join(types)))
 
1150
        return globals()[chart_type + 'Chart'](w, h, auto_scale=auto_scale,
 
1151
            x_range=x_range, y_range=y_range)
942
1152
 
943
1153
    def download(self):
944
1154
        pass