/+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 08:30:28 UTC
  • Revision ID: git-v1:33085bb9ee79265f2d97b0024c1b3bf33db09836
 - Version bump to 0.3.0
 - Fixed GPL date
 - Fixed line 80 overruns

Show diffs side-by-side

added added

removed removed

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