/+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
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):
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
    # -------------------------------------------------------------------------
351
338
        # optional arguments
352
339
        if self.title:
353
340
            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))
357
341
        if 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))
 
342
            url_bits.append('chdl=%s' % '|'.join(self.legend))
361
343
        if 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))
 
344
            url_bits.append('chco=%s' % ','.join(self.colours))
365
345
        ret = self.fill_to_url()
366
346
        if ret:
367
347
            url_bits.append(ret)
368
348
        ret = self.axis_to_url()
369
349
        if ret:
370
 
            url_bits.append(ret)                    
 
350
            url_bits.append(ret)
371
351
        if self.markers:
372
 
            url_bits.append(self.markers_to_url())        
 
352
            url_bits.append(self.markers_to_url())
373
353
        if self.line_styles:
374
354
            style = []
375
355
            for index in xrange(max(self.line_styles) + 1):
378
358
                else:
379
359
                    values = ('1', )
380
360
                style.append(','.join(values))
381
 
            url_bits.append('chls=%s' % '%7c'.join(style))
 
361
            url_bits.append('chls=%s' % '|'.join(style))
382
362
        if self.grid:
383
363
            url_bits.append('chg=%s' % self.grid)
384
364
        return url_bits
393
373
            raise BadContentTypeException('Server responded with a ' \
394
374
                'content-type of %s' % opener.headers['content-type'])
395
375
 
396
 
        open(file_name, 'wb').write(opener.read())
 
376
        open(file_name, 'wb').write(urllib.urlopen(self.get_url()).read())
397
377
 
398
378
    # Simple settings
399
379
    # -------------------------------------------------------------------------
404
384
        else:
405
385
            self.title = None
406
386
 
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
 
 
415
387
    def set_legend(self, legend):
416
388
        """legend needs to be a list, tuple or None"""
417
389
        assert(isinstance(legend, list) or isinstance(legend, tuple) or
421
393
        else:
422
394
            self.legend = None
423
395
 
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
 
 
430
396
    # Chart colours
431
397
    # -------------------------------------------------------------------------
432
398
 
440
406
                _check_colour(col)
441
407
        self.colours = colours
442
408
 
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
 
 
453
409
    # Background/Chart colours
454
410
    # -------------------------------------------------------------------------
455
411
 
464
420
        assert(angle >= 0 and angle <= 90)
465
421
        assert(len(args) % 2 == 0)
466
422
        args = list(args)  # args is probably a tuple and we need to mutate
467
 
        for a in xrange(int(len(args) / 2)):
 
423
        for a in xrange(len(args) / 2):
468
424
            col = args[a * 2]
469
425
            offset = args[a * 2 + 1]
470
426
            _check_colour(col)
491
447
                areas.append('%s,%s,%s' % (area, self.fill_types[area], \
492
448
                    self.fill_area[area]))
493
449
        if areas:
494
 
            return 'chf=' + '%7c'.join(areas)
 
450
            return 'chf=' + '|'.join(areas)
495
451
 
496
452
    # Data
497
453
    # -------------------------------------------------------------------------
515
471
        else:
516
472
            return ExtendedData
517
473
 
518
 
    def _filter_none(self, data):
519
 
        return [r for r in data if r is not None]
520
 
 
521
474
    def data_x_range(self):
522
475
        """Return a 2-tuple giving the minimum and maximum x-axis
523
476
        data range.
524
477
        """
525
478
        try:
526
 
            lower = min([min(self._filter_none(s))
527
 
                         for type, s in self.annotated_data()
 
479
            lower = min([min(s) for type, s in self.annotated_data()
528
480
                         if type == 'x'])
529
 
            upper = max([max(self._filter_none(s))
530
 
                         for type, s in self.annotated_data()
 
481
            upper = max([max(s) for type, s in self.annotated_data()
531
482
                         if type == 'x'])
532
483
            return (lower, upper)
533
484
        except ValueError:
538
489
        data range.
539
490
        """
540
491
        try:
541
 
            lower = min([min(self._filter_none(s))
542
 
                         for type, s in self.annotated_data()
 
492
            lower = min([min(s) for type, s in self.annotated_data()
543
493
                         if type == 'y'])
544
 
            upper = max([max(self._filter_none(s)) + 1
545
 
                         for type, s in self.annotated_data()
 
494
            upper = max([max(s) + 1 for type, s in self.annotated_data()
546
495
                         if type == 'y'])
547
496
            return (lower, upper)
548
497
        except ValueError:
568
517
        if x_range is None:
569
518
            x_range = self.data_x_range()
570
519
            if x_range and x_range[0] > 0:
571
 
                x_range = (x_range[0], x_range[1])
 
520
                x_range = (0, x_range[1])
572
521
        self.scaled_x_range = x_range
573
522
 
574
523
        # Determine the y-axis range for scaling.
575
524
        if y_range is None:
576
525
            y_range = self.data_y_range()
577
526
            if y_range and y_range[0] > 0:
578
 
                y_range = (y_range[0], y_range[1])
 
527
                y_range = (0, y_range[1])
579
528
        self.scaled_y_range = y_range
580
529
 
581
530
        scaled_data = []
586
535
                scale_range = y_range
587
536
            elif type == 'marker-size':
588
537
                scale_range = (0, max(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)
 
538
            scaled_data.append([data_class.scale_value(v, scale_range)
 
539
                                for v in dataset])
597
540
        return scaled_data
598
541
 
599
542
    def add_data(self, data):
606
549
        if not issubclass(data_class, Data):
607
550
            raise UnknownDataTypeException()
608
551
        if self.auto_scale:
 
552
            print data_class
609
553
            data = self.scaled_data(data_class, self.x_range, self.y_range)
610
554
        else:
611
555
            data = self.data
620
564
 
621
565
    def set_axis_labels(self, axis_type, values):
622
566
        assert(axis_type in Axis.TYPES)
623
 
        values = [urllib.quote(str(a)) for a in values]
 
567
        values = [urllib.quote(a) for a in values]
624
568
        axis_index = len(self.axis)
625
569
        axis = LabelAxis(axis_index, axis_type, values)
626
570
        self.axis.append(axis)
670
614
        url_bits = []
671
615
        url_bits.append('chxt=%s' % ','.join(available_axis))
672
616
        if label_axis:
673
 
            url_bits.append('chxl=%s' % '%7c'.join(label_axis))
 
617
            url_bits.append('chxl=%s' % '|'.join(label_axis))
674
618
        if range_axis:
675
 
            url_bits.append('chxr=%s' % '%7c'.join(range_axis))
 
619
            url_bits.append('chxr=%s' % '|'.join(range_axis))
676
620
        if positions:
677
 
            url_bits.append('chxp=%s' % '%7c'.join(positions))
 
621
            url_bits.append('chxp=%s' % '|'.join(positions))
678
622
        if styles:
679
 
            url_bits.append('chxs=%s' % '%7c'.join(styles))
 
623
            url_bits.append('chxs=%s' % '|'.join(styles))
680
624
        return '&'.join(url_bits)
681
625
 
682
626
    # Markers, Ranges and Fill area (chm)
683
627
    # -------------------------------------------------------------------------
684
628
 
685
 
    def markers_to_url(self):        
686
 
        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])
687
631
 
688
632
    def add_marker(self, index, point, marker_type, colour, size, priority=0):
689
633
        self.markers.append((marker_type, colour, str(index), str(point), \
690
634
            str(size), str(priority)))
691
635
 
692
636
    def add_horizontal_range(self, colour, start, 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)))        
 
637
        self.markers.append(('r', colour, '1', str(start), str(stop)))
703
638
 
704
639
    def add_vertical_range(self, colour, start, stop):
705
 
        self.markers.append(('R', colour, '0', str(start), str(stop)))
 
640
        self.markers.append(('R', colour, '1', str(start), str(stop)))
706
641
 
707
642
    def add_fill_range(self, colour, index_start, index_end):
708
643
        self.markers.append(('b', colour, str(index_start), str(index_end), \
895
830
            raise AbstractClassException('This is an abstract class')
896
831
        Chart.__init__(self, *args, **kwargs)
897
832
        self.pie_labels = []
898
 
        if self.y_range:
899
 
            warnings.warn('y_range is not used with %s.' % \
900
 
                (self.__class__.__name__))
901
833
 
902
834
    def set_pie_labels(self, labels):
903
835
        self.pie_labels = [urllib.quote(a) for a in labels]
905
837
    def get_url_bits(self, data_class=None):
906
838
        url_bits = Chart.get_url_bits(self, data_class=data_class)
907
839
        if self.pie_labels:
908
 
            url_bits.append('chl=%s' % '%7c'.join(self.pie_labels))
 
840
            url_bits.append('chl=%s' % '|'.join(self.pie_labels))
909
841
        return url_bits
910
842
 
911
843
    def annotated_data(self):
912
844
        # Datasets are all y-axis data. However, there should only be
913
845
        # one dataset for pie charts.
914
846
        for dataset in self.data:
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)
 
847
            yield ('y', dataset)
921
848
 
922
849
 
923
850
class PieChart2D(PieChart):
960
887
        Chart.__init__(self, *args, **kwargs)
961
888
        self.geo_area = 'world'
962
889
        self.codes = []
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
 
        
 
890
 
990
891
    def type_to_url(self):
991
892
        return 'cht=t'
992
893
 
993
894
    def set_codes(self, 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)
 
895
        self.codes = codes
1029
896
 
1030
897
    def get_url_bits(self, data_class=None):
1031
898
        url_bits = Chart.get_url_bits(self, data_class=data_class)
1034
901
            url_bits.append('chld=%s' % ''.join(self.codes))
1035
902
        return url_bits
1036
903
 
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
 
 
1046
904
 
1047
905
class GoogleOMeterChart(PieChart):
1048
906
    """Inheriting from PieChart because of similar labeling"""
1049
907
 
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
 
 
1056
908
    def type_to_url(self):
1057
909
        return 'cht=gom'
1058
910
 
1059
911
 
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
 
 
1092
912
class ChartGrammar(object):
1093
913
 
1094
 
    def __init__(self):
1095
 
        self.grammar = None
1096
 
        self.chart = None
1097
 
 
1098
 
    def parse(self, grammar):
 
914
    def __init__(self, grammar):
1099
915
        self.grammar = grammar
1100
916
        self.chart = self.create_chart_instance()
1101
917
 
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
 
 
1116
918
    @staticmethod
1117
919
    def get_possible_chart_types():
1118
920
        possible_charts = []
1119
 
        for cls_name in globals().keys():
 
921
        for cls_name in globals():
1120
922
            if not cls_name.endswith('Chart'):
1121
923
                continue
1122
924
            cls = globals()[cls_name]
1123
925
            # Check if it is an abstract class
1124
926
            try:
1125
 
                a = cls(1, 1, auto_scale=False)
1126
 
                del a
 
927
                cls(1, 1)
1127
928
            except AbstractClassException:
1128
929
                continue
1129
930
            # Strip off "Class"
1130
931
            possible_charts.append(cls_name[:-5])
1131
932
        return possible_charts
1132
933
 
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
 
934
    def create_chart_instance(self):
1137
935
        assert('w' in grammar)  # width is required
1138
936
        assert('h' in grammar)  # height is required
1139
937
        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)
1146
938
        types = ChartGrammar.get_possible_chart_types()
1147
 
        if chart_type not in types:
 
939
        if grammar['type'] not in types:
1148
940
            raise UnknownChartType('%s is an unknown chart type. Possible '
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)
 
941
                'chart types are %s' % (grammar['type'], ','.join(types)))
1152
942
 
1153
943
    def download(self):
1154
944
        pass