/+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: Gerald Kaszuba
  • Date: 2010-12-24 05:18:55 UTC
  • Revision ID: git-v1:1a633e0584a557e812fae20deceaef514a515f62
gitignore compiled py files and setup.py build dir

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
317
332
        self.markers = []
318
333
        self.line_styles = {}
319
334
        self.grid = None
 
335
        self.title_colour = None
 
336
        self.title_font_size = None
320
337
 
321
338
    # URL generation
322
339
    # -------------------------------------------------------------------------
334
351
        # optional arguments
335
352
        if self.title:
336
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))
337
357
        if self.legend:
338
 
            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))
339
361
        if self.colours:
340
 
            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))
341
365
        ret = self.fill_to_url()
342
366
        if ret:
343
367
            url_bits.append(ret)
344
368
        ret = self.axis_to_url()
345
369
        if ret:
346
 
            url_bits.append(ret)
 
370
            url_bits.append(ret)                    
347
371
        if self.markers:
348
 
            url_bits.append(self.markers_to_url())
 
372
            url_bits.append(self.markers_to_url())        
349
373
        if self.line_styles:
350
374
            style = []
351
375
            for index in xrange(max(self.line_styles) + 1):
354
378
                else:
355
379
                    values = ('1', )
356
380
                style.append(','.join(values))
357
 
            url_bits.append('chls=%s' % '|'.join(style))
 
381
            url_bits.append('chls=%s' % '%7c'.join(style))
358
382
        if self.grid:
359
383
            url_bits.append('chg=%s' % self.grid)
360
384
        return url_bits
369
393
            raise BadContentTypeException('Server responded with a ' \
370
394
                'content-type of %s' % opener.headers['content-type'])
371
395
 
372
 
        open(file_name, 'wb').write(urllib.urlopen(self.get_url()).read())
 
396
        open(file_name, 'wb').write(opener.read())
373
397
 
374
398
    # Simple settings
375
399
    # -------------------------------------------------------------------------
380
404
        else:
381
405
            self.title = None
382
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
 
383
415
    def set_legend(self, legend):
384
416
        """legend needs to be a list, tuple or None"""
385
417
        assert(isinstance(legend, list) or isinstance(legend, tuple) or
389
421
        else:
390
422
            self.legend = None
391
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
 
392
430
    # Chart colours
393
431
    # -------------------------------------------------------------------------
394
432
 
402
440
                _check_colour(col)
403
441
        self.colours = colours
404
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
 
405
453
    # Background/Chart colours
406
454
    # -------------------------------------------------------------------------
407
455
 
416
464
        assert(angle >= 0 and angle <= 90)
417
465
        assert(len(args) % 2 == 0)
418
466
        args = list(args)  # args is probably a tuple and we need to mutate
419
 
        for a in xrange(len(args) / 2):
 
467
        for a in xrange(int(len(args) / 2)):
420
468
            col = args[a * 2]
421
469
            offset = args[a * 2 + 1]
422
470
            _check_colour(col)
443
491
                areas.append('%s,%s,%s' % (area, self.fill_types[area], \
444
492
                    self.fill_area[area]))
445
493
        if areas:
446
 
            return 'chf=' + '|'.join(areas)
 
494
            return 'chf=' + '%7c'.join(areas)
447
495
 
448
496
    # Data
449
497
    # -------------------------------------------------------------------------
467
515
        else:
468
516
            return ExtendedData
469
517
 
 
518
    def _filter_none(self, data):
 
519
        return [r for r in data if r is not None]
 
520
 
470
521
    def data_x_range(self):
471
522
        """Return a 2-tuple giving the minimum and maximum x-axis
472
523
        data range.
473
524
        """
474
525
        try:
475
 
            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()
476
528
                         if type == 'x'])
477
 
            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()
478
531
                         if type == 'x'])
479
532
            return (lower, upper)
480
533
        except ValueError:
485
538
        data range.
486
539
        """
487
540
        try:
488
 
            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()
489
543
                         if type == 'y'])
490
 
            upper = max([max(s) for type, s in self.annotated_data()
 
544
            upper = max([max(self._filter_none(s)) + 1
 
545
                         for type, s in self.annotated_data()
491
546
                         if type == 'y'])
492
547
            return (lower, upper)
493
548
        except ValueError:
513
568
        if x_range is None:
514
569
            x_range = self.data_x_range()
515
570
            if x_range and x_range[0] > 0:
516
 
                x_range = (0, x_range[1])
 
571
                x_range = (x_range[0], x_range[1])
517
572
        self.scaled_x_range = x_range
518
573
 
519
574
        # Determine the y-axis range for scaling.
520
575
        if y_range is None:
521
576
            y_range = self.data_y_range()
522
577
            if y_range and y_range[0] > 0:
523
 
                y_range = (0, y_range[1])
 
578
                y_range = (y_range[0], y_range[1])
524
579
        self.scaled_y_range = y_range
525
580
 
526
581
        scaled_data = []
531
586
                scale_range = y_range
532
587
            elif type == 'marker-size':
533
588
                scale_range = (0, max(dataset))
534
 
            scaled_data.append([data_class.scale_value(v, scale_range)
535
 
                                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)
536
597
        return scaled_data
537
598
 
538
599
    def add_data(self, data):
559
620
 
560
621
    def set_axis_labels(self, axis_type, values):
561
622
        assert(axis_type in Axis.TYPES)
562
 
        values = [ urllib.quote(a) for a in values ]
 
623
        values = [urllib.quote(str(a)) for a in values]
563
624
        axis_index = len(self.axis)
564
625
        axis = LabelAxis(axis_index, axis_type, values)
565
626
        self.axis.append(axis)
609
670
        url_bits = []
610
671
        url_bits.append('chxt=%s' % ','.join(available_axis))
611
672
        if label_axis:
612
 
            url_bits.append('chxl=%s' % '|'.join(label_axis))
 
673
            url_bits.append('chxl=%s' % '%7c'.join(label_axis))
613
674
        if range_axis:
614
 
            url_bits.append('chxr=%s' % '|'.join(range_axis))
 
675
            url_bits.append('chxr=%s' % '%7c'.join(range_axis))
615
676
        if positions:
616
 
            url_bits.append('chxp=%s' % '|'.join(positions))
 
677
            url_bits.append('chxp=%s' % '%7c'.join(positions))
617
678
        if styles:
618
 
            url_bits.append('chxs=%s' % '|'.join(styles))
 
679
            url_bits.append('chxs=%s' % '%7c'.join(styles))
619
680
        return '&'.join(url_bits)
620
681
 
621
682
    # Markers, Ranges and Fill area (chm)
622
683
    # -------------------------------------------------------------------------
623
684
 
624
 
    def markers_to_url(self):
625
 
        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])
626
687
 
627
688
    def add_marker(self, index, point, marker_type, colour, size, priority=0):
628
689
        self.markers.append((marker_type, colour, str(index), str(point), \
629
690
            str(size), str(priority)))
630
691
 
631
692
    def add_horizontal_range(self, colour, start, stop):
632
 
        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)))        
633
703
 
634
704
    def add_vertical_range(self, colour, start, stop):
635
 
        self.markers.append(('R', colour, '1', str(start), str(stop)))
 
705
        self.markers.append(('R', colour, '0', str(start), str(stop)))
636
706
 
637
707
    def add_fill_range(self, colour, index_start, index_end):
638
708
        self.markers.append(('b', colour, str(index_start), str(index_end), \
675
745
            # markers.
676
746
            yield ('marker-size', self.data[2])
677
747
 
 
748
 
678
749
class LineChart(Chart):
679
750
 
680
751
    def __init__(self, *args, **kwargs):
681
 
        assert(type(self) != LineChart)  # This is an abstract class
 
752
        if type(self) == LineChart:
 
753
            raise AbstractClassException('This is an abstract class')
682
754
        Chart.__init__(self, *args, **kwargs)
683
755
 
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
756
 
689
757
class SimpleLineChart(LineChart):
690
758
 
696
764
        for dataset in self.data:
697
765
            yield ('y', dataset)
698
766
 
 
767
 
699
768
class SparkLineChart(SimpleLineChart):
700
769
 
701
770
    def type_to_url(self):
702
771
        return 'cht=ls'
703
772
 
 
773
 
704
774
class XYLineChart(LineChart):
705
775
 
706
776
    def type_to_url(self):
714
784
            else:
715
785
                yield ('y', dataset)
716
786
 
 
787
 
717
788
class BarChart(Chart):
718
789
 
719
790
    def __init__(self, *args, **kwargs):
720
 
        assert(type(self) != BarChart)  # This is an abstract class
 
791
        if type(self) == BarChart:
 
792
            raise AbstractClassException('This is an abstract class')
721
793
        Chart.__init__(self, *args, **kwargs)
722
794
        self.bar_width = None
723
795
        self.zero_lines = {}
762
834
class GroupedBarChart(BarChart):
763
835
 
764
836
    def __init__(self, *args, **kwargs):
765
 
        assert(type(self) != GroupedBarChart)  # This is an abstract class
 
837
        if type(self) == GroupedBarChart:
 
838
            raise AbstractClassException('This is an abstract class')
766
839
        BarChart.__init__(self, *args, **kwargs)
767
840
        self.bar_spacing = None
768
841
        self.group_spacing = None
782
855
            skip_chbh=True)
783
856
        if self.group_spacing is not None:
784
857
            if self.bar_spacing is None:
785
 
                raise InvalidParametersException('Bar spacing is required to ' \
786
 
                    'be set when setting group spacing')
 
858
                raise InvalidParametersException('Bar spacing is required ' \
 
859
                    'to be set when setting group spacing')
787
860
            if self.bar_width is None:
788
861
                raise InvalidParametersException('Bar width is required to ' \
789
862
                    'be set when setting bar spacing')
818
891
class PieChart(Chart):
819
892
 
820
893
    def __init__(self, *args, **kwargs):
821
 
        assert(type(self) != PieChart)  # This is an abstract class
 
894
        if type(self) == PieChart:
 
895
            raise AbstractClassException('This is an abstract class')
822
896
        Chart.__init__(self, *args, **kwargs)
823
897
        self.pie_labels = []
 
898
        if self.y_range:
 
899
            warnings.warn('y_range is not used with %s.' % \
 
900
                (self.__class__.__name__))
824
901
 
825
902
    def set_pie_labels(self, labels):
826
903
        self.pie_labels = [urllib.quote(a) for a in labels]
828
905
    def get_url_bits(self, data_class=None):
829
906
        url_bits = Chart.get_url_bits(self, data_class=data_class)
830
907
        if self.pie_labels:
831
 
            url_bits.append('chl=%s' % '|'.join(self.pie_labels))
 
908
            url_bits.append('chl=%s' % '%7c'.join(self.pie_labels))
832
909
        return url_bits
833
910
 
834
911
    def annotated_data(self):
835
912
        # Datasets are all y-axis data. However, there should only be
836
913
        # one dataset for pie charts.
837
914
        for dataset in self.data:
838
 
            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)
839
921
 
840
922
 
841
923
class PieChart2D(PieChart):
865
947
    def type_to_url(self):
866
948
        return 'cht=r'
867
949
 
 
950
 
868
951
class SplineRadarChart(RadarChart):
869
952
 
870
953
    def type_to_url(self):
877
960
        Chart.__init__(self, *args, **kwargs)
878
961
        self.geo_area = 'world'
879
962
        self.codes = []
880
 
 
 
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
        
881
990
    def type_to_url(self):
882
991
        return 'cht=t'
883
992
 
884
993
    def set_codes(self, codes):
885
 
        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)
886
1029
 
887
1030
    def get_url_bits(self, data_class=None):
888
1031
        url_bits = Chart.get_url_bits(self, data_class=data_class)
891
1034
            url_bits.append('chld=%s' % ''.join(self.codes))
892
1035
        return url_bits
893
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
 
894
1046
 
895
1047
class GoogleOMeterChart(PieChart):
896
1048
    """Inheriting from PieChart because of similar labeling"""
897
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
 
898
1056
    def type_to_url(self):
899
1057
        return 'cht=gom'
900
1058
 
901
1059
 
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()
 
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
class ChartGrammar(object):
 
1093
 
 
1094
    def __init__(self):
 
1095
        self.grammar = None
 
1096
        self.chart = None
 
1097
 
 
1098
    def parse(self, grammar):
 
1099
        self.grammar = grammar
 
1100
        self.chart = self.create_chart_instance()
 
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
 
 
1116
    @staticmethod
 
1117
    def get_possible_chart_types():
 
1118
        possible_charts = []
 
1119
        for cls_name in globals().keys():
 
1120
            if not cls_name.endswith('Chart'):
 
1121
                continue
 
1122
            cls = globals()[cls_name]
 
1123
            # Check if it is an abstract class
 
1124
            try:
 
1125
                a = cls(1, 1, auto_scale=False)
 
1126
                del a
 
1127
            except AbstractClassException:
 
1128
                continue
 
1129
            # Strip off "Class"
 
1130
            possible_charts.append(cls_name[:-5])
 
1131
        return possible_charts
 
1132
 
 
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
 
1137
        assert('w' in grammar)  # width is required
 
1138
        assert('h' in grammar)  # height is required
 
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)
 
1146
        types = ChartGrammar.get_possible_chart_types()
 
1147
        if chart_type not in types:
 
1148
            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)
 
1152
 
 
1153
    def download(self):
 
1154
        pass
966
1155