/+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 07:41:54 UTC
  • Revision ID: git-v1:af07747db18840d2b5e1dd9be42f20009f922f06
Added mapchart example

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-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
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.2.2'
 
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
 
72
89
# Data Classes
73
90
# -----------------------------------------------------------------------------
74
91
 
76
93
class Data(object):
77
94
 
78
95
    def __init__(self, data):
79
 
        assert(type(self) != Data)  # This is an abstract class
 
96
        if type(self) == Data:
 
97
            raise AbstractClassException('This is an abstract class')
80
98
        self.data = data
81
99
 
82
100
    @classmethod
83
101
    def float_scale_value(cls, value, range):
84
102
        lower, upper = range
85
 
        max_value = cls.max_value()
86
 
        scaled = (value-lower) * (float(max_value)/(upper-lower))
 
103
        assert(upper > lower)
 
104
        scaled = (value - lower) * (cls.max_value / (upper - lower))
87
105
        return scaled
88
106
 
89
107
    @classmethod
90
108
    def clip_value(cls, value):
91
 
        clipped = max(0, min(value, cls.max_value()))
92
 
        return clipped
 
109
        return max(0, min(value, cls.max_value))
93
110
 
94
111
    @classmethod
95
112
    def int_scale_value(cls, value, range):
96
 
        scaled = int(round(cls.float_scale_value(value, range)))
97
 
        return scaled
 
113
        return int(round(cls.float_scale_value(value, range)))
98
114
 
99
115
    @classmethod
100
116
    def scale_value(cls, value, range):
101
117
        scaled = cls.int_scale_value(value, range)
102
118
        clipped = cls.clip_value(scaled)
 
119
        Data.check_clip(scaled, clipped)
103
120
        return clipped
104
121
 
 
122
    @staticmethod
 
123
    def check_clip(scaled, clipped):
 
124
        if clipped != scaled:
 
125
            warnings.warn('One or more of of your data points has been '
 
126
                'clipped because it is out of range.')
 
127
 
 
128
 
105
129
class SimpleData(Data):
 
130
 
 
131
    max_value = 61
106
132
    enc_map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
107
133
 
108
134
    def __repr__(self):
109
 
        max_value = self.max_value()
110
135
        encoded_data = []
111
136
        for data in self.data:
112
137
            sub_data = []
113
138
            for value in data:
114
139
                if value is None:
115
140
                    sub_data.append('_')
116
 
                elif value >= 0 and value <= max_value:
 
141
                elif value >= 0 and value <= self.max_value:
117
142
                    sub_data.append(SimpleData.enc_map[value])
118
143
                else:
119
144
                    raise DataOutOfRangeException('cannot encode value: %d'
121
146
            encoded_data.append(''.join(sub_data))
122
147
        return 'chd=s:' + ','.join(encoded_data)
123
148
 
124
 
    @staticmethod
125
 
    def max_value():
126
 
        return 61
127
149
 
128
150
class TextData(Data):
129
151
 
 
152
    max_value = 100
 
153
 
130
154
    def __repr__(self):
131
 
        max_value = self.max_value()
132
155
        encoded_data = []
133
156
        for data in self.data:
134
157
            sub_data = []
135
158
            for value in data:
136
159
                if value is None:
137
160
                    sub_data.append(-1)
138
 
                elif value >= 0 and value <= max_value:
 
161
                elif value >= 0 and value <= self.max_value:
139
162
                    sub_data.append("%.1f" % float(value))
140
163
                else:
141
164
                    raise DataOutOfRangeException()
142
165
            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
 
166
        return 'chd=t:' + '%7c'.join(encoded_data)
159
167
 
160
168
    @classmethod
161
169
    def scale_value(cls, value, range):
162
170
        # use float values instead of integers because we don't need an encode
163
171
        # map index
164
 
        scaled = cls.float_scale_value(value,range)
 
172
        scaled = cls.float_scale_value(value, range)
165
173
        clipped = cls.clip_value(scaled)
 
174
        Data.check_clip(scaled, clipped)
166
175
        return clipped
167
176
 
 
177
 
168
178
class ExtendedData(Data):
 
179
 
 
180
    max_value = 4095
169
181
    enc_map = \
170
182
        'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-.'
171
183
 
172
184
    def __repr__(self):
173
 
        max_value = self.max_value()
174
185
        encoded_data = []
175
186
        enc_size = len(ExtendedData.enc_map)
176
187
        for data in self.data:
178
189
            for value in data:
179
190
                if value is None:
180
191
                    sub_data.append('__')
181
 
                elif value >= 0 and value <= max_value:
 
192
                elif value >= 0 and value <= self.max_value:
182
193
                    first, second = divmod(int(value), enc_size)
183
194
                    sub_data.append('%s%s' % (
184
195
                        ExtendedData.enc_map[first],
190
201
            encoded_data.append(''.join(sub_data))
191
202
        return 'chd=e:' + ','.join(encoded_data)
192
203
 
193
 
    @staticmethod
194
 
    def max_value():
195
 
        return 4095
196
 
 
197
204
 
198
205
# Axis Classes
199
206
# -----------------------------------------------------------------------------
200
207
 
201
208
 
202
209
class Axis(object):
 
210
 
203
211
    BOTTOM = 'x'
204
212
    TOP = 't'
205
213
    LEFT = 'y'
250
258
        self.values = [str(a) for a in values]
251
259
 
252
260
    def __repr__(self):
253
 
        return '%i:|%s' % (self.axis_index, '|'.join(self.values))
 
261
        return '%i:%%7c%s' % (self.axis_index, '%7c'.join(self.values))
254
262
 
255
263
 
256
264
class RangeAxis(Axis):
284
292
    LINEAR_STRIPES = 'ls'
285
293
 
286
294
    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
 
295
            auto_scale=True, x_range=None, y_range=None,
 
296
            colours_within_series=None):
 
297
        if type(self) == Chart:
 
298
            raise AbstractClassException('This is an abstract class')
289
299
        assert(isinstance(width, int))
290
300
        assert(isinstance(height, int))
291
301
        self.width = width
292
302
        self.height = height
293
303
        self.data = []
294
304
        self.set_title(title)
 
305
        self.set_title_style(None, None)
295
306
        self.set_legend(legend)
 
307
        self.set_legend_position(None)
296
308
        self.set_colours(colours)
 
309
        self.set_colours_within_series(colours_within_series)
297
310
 
298
311
        # 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
 
312
        self.auto_scale = auto_scale  # Whether to automatically scale data
 
313
        self.x_range = x_range  # (min, max) x-axis range for scaling
 
314
        self.y_range = y_range  # (min, max) y-axis range for scaling
302
315
        self.scaled_data_class = None
303
316
        self.scaled_x_range = None
304
317
        self.scaled_y_range = None
334
347
        # optional arguments
335
348
        if self.title:
336
349
            url_bits.append('chtt=%s' % self.title)
 
350
        if self.title_colour and self.title_font_size:
 
351
            url_bits.append('chts=%s,%s' % (self.title_colour, \
 
352
                self.title_font_size))
337
353
        if self.legend:
338
 
            url_bits.append('chdl=%s' % '|'.join(self.legend))
 
354
            url_bits.append('chdl=%s' % '%7c'.join(self.legend))
 
355
        if self.legend_position:
 
356
            url_bits.append('chdlp=%s' % (self.legend_position))
339
357
        if self.colours:
340
 
            url_bits.append('chco=%s' % ','.join(self.colours))
 
358
            url_bits.append('chco=%s' % ','.join(self.colours))            
 
359
        if self.colours_within_series:
 
360
            url_bits.append('chco=%s' % '%7c'.join(self.colours_within_series))
341
361
        ret = self.fill_to_url()
342
362
        if ret:
343
363
            url_bits.append(ret)
344
364
        ret = self.axis_to_url()
345
365
        if ret:
346
 
            url_bits.append(ret)
 
366
            url_bits.append(ret)                    
347
367
        if self.markers:
348
 
            url_bits.append(self.markers_to_url())
 
368
            url_bits.append(self.markers_to_url())        
349
369
        if self.line_styles:
350
370
            style = []
351
371
            for index in xrange(max(self.line_styles) + 1):
354
374
                else:
355
375
                    values = ('1', )
356
376
                style.append(','.join(values))
357
 
            url_bits.append('chls=%s' % '|'.join(style))
 
377
            url_bits.append('chls=%s' % '%7c'.join(style))
358
378
        if self.grid:
359
379
            url_bits.append('chg=%s' % self.grid)
360
380
        return url_bits
369
389
            raise BadContentTypeException('Server responded with a ' \
370
390
                'content-type of %s' % opener.headers['content-type'])
371
391
 
372
 
        open(file_name, 'wb').write(urllib.urlopen(self.get_url()).read())
 
392
        open(file_name, 'wb').write(opener.read())
373
393
 
374
394
    # Simple settings
375
395
    # -------------------------------------------------------------------------
380
400
        else:
381
401
            self.title = None
382
402
 
 
403
    def set_title_style(self, colour, font_size):
 
404
        if not colour is None:
 
405
            _check_colour(colour)
 
406
        self.title_colour = colour
 
407
        self.title_font_size = font_size
 
408
 
383
409
    def set_legend(self, legend):
384
410
        """legend needs to be a list, tuple or None"""
385
411
        assert(isinstance(legend, list) or isinstance(legend, tuple) or
389
415
        else:
390
416
            self.legend = None
391
417
 
 
418
    def set_legend_position(self, legend_position):
 
419
        if legend_position:
 
420
            self.legend_position = urllib.quote(legend_position)
 
421
        else:    
 
422
            self.legend_position = None
 
423
 
392
424
    # Chart colours
393
425
    # -------------------------------------------------------------------------
394
426
 
402
434
                _check_colour(col)
403
435
        self.colours = colours
404
436
 
 
437
    def set_colours_within_series(self, colours):
 
438
        # colours needs to be a list, tuple or None
 
439
        assert(isinstance(colours, list) or isinstance(colours, tuple) or
 
440
            colours is None)
 
441
        # make sure the colours are in the right format
 
442
        if colours:
 
443
            for col in colours:
 
444
                _check_colour(col)
 
445
        self.colours_within_series = colours        
 
446
 
405
447
    # Background/Chart colours
406
448
    # -------------------------------------------------------------------------
407
449
 
416
458
        assert(angle >= 0 and angle <= 90)
417
459
        assert(len(args) % 2 == 0)
418
460
        args = list(args)  # args is probably a tuple and we need to mutate
419
 
        for a in xrange(len(args) / 2):
 
461
        for a in xrange(int(len(args) / 2)):
420
462
            col = args[a * 2]
421
463
            offset = args[a * 2 + 1]
422
464
            _check_colour(col)
443
485
                areas.append('%s,%s,%s' % (area, self.fill_types[area], \
444
486
                    self.fill_area[area]))
445
487
        if areas:
446
 
            return 'chf=' + '|'.join(areas)
 
488
            return 'chf=' + '%7c'.join(areas)
447
489
 
448
490
    # Data
449
491
    # -------------------------------------------------------------------------
467
509
        else:
468
510
            return ExtendedData
469
511
 
 
512
    def _filter_none(self, data):
 
513
        return [r for r in data if r is not None]
 
514
 
470
515
    def data_x_range(self):
471
516
        """Return a 2-tuple giving the minimum and maximum x-axis
472
517
        data range.
473
518
        """
474
519
        try:
475
 
            lower = min([min(s) for type, s in self.annotated_data()
 
520
            lower = min([min(self._filter_none(s))
 
521
                         for type, s in self.annotated_data()
476
522
                         if type == 'x'])
477
 
            upper = max([max(s) for type, s in self.annotated_data()
 
523
            upper = max([max(self._filter_none(s))
 
524
                         for type, s in self.annotated_data()
478
525
                         if type == 'x'])
479
526
            return (lower, upper)
480
527
        except ValueError:
485
532
        data range.
486
533
        """
487
534
        try:
488
 
            lower = min([min(s) for type, s in self.annotated_data()
 
535
            lower = min([min(self._filter_none(s))
 
536
                         for type, s in self.annotated_data()
489
537
                         if type == 'y'])
490
 
            upper = max([max(s) for type, s in self.annotated_data()
 
538
            upper = max([max(self._filter_none(s)) + 1
 
539
                         for type, s in self.annotated_data()
491
540
                         if type == 'y'])
492
541
            return (lower, upper)
493
542
        except ValueError:
513
562
        if x_range is None:
514
563
            x_range = self.data_x_range()
515
564
            if x_range and x_range[0] > 0:
516
 
                x_range = (0, x_range[1])
 
565
                x_range = (x_range[0], x_range[1])
517
566
        self.scaled_x_range = x_range
518
567
 
519
568
        # Determine the y-axis range for scaling.
520
569
        if y_range is None:
521
570
            y_range = self.data_y_range()
522
571
            if y_range and y_range[0] > 0:
523
 
                y_range = (0, y_range[1])
 
572
                y_range = (y_range[0], y_range[1])
524
573
        self.scaled_y_range = y_range
525
574
 
526
575
        scaled_data = []
531
580
                scale_range = y_range
532
581
            elif type == 'marker-size':
533
582
                scale_range = (0, max(dataset))
534
 
            scaled_data.append([data_class.scale_value(v, scale_range)
535
 
                                for v in dataset])
 
583
            scaled_dataset = []
 
584
            for v in dataset:
 
585
                if v is None:
 
586
                    scaled_dataset.append(None)
 
587
                else:
 
588
                    scaled_dataset.append(
 
589
                        data_class.scale_value(v, scale_range))
 
590
            scaled_data.append(scaled_dataset)
536
591
        return scaled_data
537
592
 
538
593
    def add_data(self, data):
559
614
 
560
615
    def set_axis_labels(self, axis_type, values):
561
616
        assert(axis_type in Axis.TYPES)
562
 
        values = [ urllib.quote(a) for a in values ]
 
617
        values = [urllib.quote(str(a)) for a in values]
563
618
        axis_index = len(self.axis)
564
619
        axis = LabelAxis(axis_index, axis_type, values)
565
620
        self.axis.append(axis)
609
664
        url_bits = []
610
665
        url_bits.append('chxt=%s' % ','.join(available_axis))
611
666
        if label_axis:
612
 
            url_bits.append('chxl=%s' % '|'.join(label_axis))
 
667
            url_bits.append('chxl=%s' % '%7c'.join(label_axis))
613
668
        if range_axis:
614
 
            url_bits.append('chxr=%s' % '|'.join(range_axis))
 
669
            url_bits.append('chxr=%s' % '%7c'.join(range_axis))
615
670
        if positions:
616
 
            url_bits.append('chxp=%s' % '|'.join(positions))
 
671
            url_bits.append('chxp=%s' % '%7c'.join(positions))
617
672
        if styles:
618
 
            url_bits.append('chxs=%s' % '|'.join(styles))
 
673
            url_bits.append('chxs=%s' % '%7c'.join(styles))
619
674
        return '&'.join(url_bits)
620
675
 
621
676
    # Markers, Ranges and Fill area (chm)
622
677
    # -------------------------------------------------------------------------
623
678
 
624
 
    def markers_to_url(self):
625
 
        return 'chm=%s' % '|'.join([','.join(a) for a in self.markers])
 
679
    def markers_to_url(self):        
 
680
        return 'chm=%s' % '%7c'.join([','.join(a) for a in self.markers])
626
681
 
627
682
    def add_marker(self, index, point, marker_type, colour, size, priority=0):
628
683
        self.markers.append((marker_type, colour, str(index), str(point), \
629
684
            str(size), str(priority)))
630
685
 
631
686
    def add_horizontal_range(self, colour, start, stop):
632
 
        self.markers.append(('r', colour, '1', str(start), str(stop)))
 
687
        self.markers.append(('r', colour, '0', str(start), str(stop)))
 
688
 
 
689
    def add_data_line(self, colour, data_set, size, priority=0):
 
690
        self.markers.append(('D', colour, str(data_set), '0', str(size), \
 
691
            str(priority)))
 
692
 
 
693
    def add_marker_text(self, string, colour, data_set, data_point, size, \
 
694
            priority=0):
 
695
        self.markers.append((str(string), colour, str(data_set), \
 
696
            str(data_point), str(size), str(priority)))        
633
697
 
634
698
    def add_vertical_range(self, colour, start, stop):
635
 
        self.markers.append(('R', colour, '1', str(start), str(stop)))
 
699
        self.markers.append(('R', colour, '0', str(start), str(stop)))
636
700
 
637
701
    def add_fill_range(self, colour, index_start, index_end):
638
702
        self.markers.append(('b', colour, str(index_start), str(index_end), \
675
739
            # markers.
676
740
            yield ('marker-size', self.data[2])
677
741
 
 
742
 
678
743
class LineChart(Chart):
679
744
 
680
745
    def __init__(self, *args, **kwargs):
681
 
        assert(type(self) != LineChart)  # This is an abstract class
 
746
        if type(self) == LineChart:
 
747
            raise AbstractClassException('This is an abstract class')
682
748
        Chart.__init__(self, *args, **kwargs)
683
749
 
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
750
 
689
751
class SimpleLineChart(LineChart):
690
752
 
696
758
        for dataset in self.data:
697
759
            yield ('y', dataset)
698
760
 
 
761
 
699
762
class SparkLineChart(SimpleLineChart):
700
763
 
701
764
    def type_to_url(self):
702
765
        return 'cht=ls'
703
766
 
 
767
 
704
768
class XYLineChart(LineChart):
705
769
 
706
770
    def type_to_url(self):
714
778
            else:
715
779
                yield ('y', dataset)
716
780
 
 
781
 
717
782
class BarChart(Chart):
718
783
 
719
784
    def __init__(self, *args, **kwargs):
720
 
        assert(type(self) != BarChart)  # This is an abstract class
 
785
        if type(self) == BarChart:
 
786
            raise AbstractClassException('This is an abstract class')
721
787
        Chart.__init__(self, *args, **kwargs)
722
788
        self.bar_width = None
723
789
        self.zero_lines = {}
762
828
class GroupedBarChart(BarChart):
763
829
 
764
830
    def __init__(self, *args, **kwargs):
765
 
        assert(type(self) != GroupedBarChart)  # This is an abstract class
 
831
        if type(self) == GroupedBarChart:
 
832
            raise AbstractClassException('This is an abstract class')
766
833
        BarChart.__init__(self, *args, **kwargs)
767
834
        self.bar_spacing = None
768
835
        self.group_spacing = None
782
849
            skip_chbh=True)
783
850
        if self.group_spacing is not None:
784
851
            if self.bar_spacing is None:
785
 
                raise InvalidParametersException('Bar spacing is required to ' \
786
 
                    'be set when setting group spacing')
 
852
                raise InvalidParametersException('Bar spacing is required ' \
 
853
                    'to be set when setting group spacing')
787
854
            if self.bar_width is None:
788
855
                raise InvalidParametersException('Bar width is required to ' \
789
856
                    'be set when setting bar spacing')
818
885
class PieChart(Chart):
819
886
 
820
887
    def __init__(self, *args, **kwargs):
821
 
        assert(type(self) != PieChart)  # This is an abstract class
 
888
        if type(self) == PieChart:
 
889
            raise AbstractClassException('This is an abstract class')
822
890
        Chart.__init__(self, *args, **kwargs)
823
891
        self.pie_labels = []
 
892
        if self.y_range:
 
893
            warnings.warn('y_range is not used with %s.' % \
 
894
                (self.__class__.__name__))
824
895
 
825
896
    def set_pie_labels(self, labels):
826
897
        self.pie_labels = [urllib.quote(a) for a in labels]
828
899
    def get_url_bits(self, data_class=None):
829
900
        url_bits = Chart.get_url_bits(self, data_class=data_class)
830
901
        if self.pie_labels:
831
 
            url_bits.append('chl=%s' % '|'.join(self.pie_labels))
 
902
            url_bits.append('chl=%s' % '%7c'.join(self.pie_labels))
832
903
        return url_bits
833
904
 
834
905
    def annotated_data(self):
835
906
        # Datasets are all y-axis data. However, there should only be
836
907
        # one dataset for pie charts.
837
908
        for dataset in self.data:
838
 
            yield ('y', dataset)
 
909
            yield ('x', dataset)
 
910
 
 
911
    def scaled_data(self, data_class, x_range=None, y_range=None):
 
912
        if not x_range:
 
913
            x_range = [0, sum(self.data[0])]
 
914
        return Chart.scaled_data(self, data_class, x_range, self.y_range)
839
915
 
840
916
 
841
917
class PieChart2D(PieChart):
865
941
    def type_to_url(self):
866
942
        return 'cht=r'
867
943
 
 
944
 
868
945
class SplineRadarChart(RadarChart):
869
946
 
870
947
    def type_to_url(self):
895
972
class GoogleOMeterChart(PieChart):
896
973
    """Inheriting from PieChart because of similar labeling"""
897
974
 
 
975
    def __init__(self, *args, **kwargs):
 
976
        PieChart.__init__(self, *args, **kwargs)
 
977
        if self.auto_scale and not self.x_range:
 
978
            warnings.warn('Please specify an x_range with GoogleOMeterChart, '
 
979
                'otherwise one arrow will always be at the max.')
 
980
 
898
981
    def type_to_url(self):
899
982
        return 'cht=gom'
900
983
 
901
984
 
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()
 
985
class QRChart(Chart):
 
986
 
 
987
    def __init__(self, *args, **kwargs):
 
988
        Chart.__init__(self, *args, **kwargs)
 
989
        self.encoding = None
 
990
        self.ec_level = None
 
991
        self.margin = None
 
992
 
 
993
    def type_to_url(self):
 
994
        return 'cht=qr'
 
995
 
 
996
    def data_to_url(self, data_class=None):
 
997
        if not self.data:
 
998
            raise NoDataGivenException()
 
999
        return 'chl=%s' % urllib.quote(self.data[0])
 
1000
 
 
1001
    def get_url_bits(self, data_class=None):
 
1002
        url_bits = Chart.get_url_bits(self, data_class=data_class)
 
1003
        if self.encoding:
 
1004
            url_bits.append('choe=%s' % self.encoding)
 
1005
        if self.ec_level:
 
1006
            url_bits.append('chld=%s%%7c%s' % (self.ec_level, self.margin))
 
1007
        return url_bits
 
1008
 
 
1009
    def set_encoding(self, encoding):
 
1010
        self.encoding = encoding
 
1011
 
 
1012
    def set_ec(self, level, margin):
 
1013
        self.ec_level = level
 
1014
        self.margin = margin
 
1015
 
 
1016
 
 
1017
class ChartGrammar(object):
 
1018
 
 
1019
    def __init__(self):
 
1020
        self.grammar = None
 
1021
        self.chart = None
 
1022
 
 
1023
    def parse(self, grammar):
 
1024
        self.grammar = grammar
 
1025
        self.chart = self.create_chart_instance()
 
1026
 
 
1027
        for attr in self.grammar:
 
1028
            if attr in ('w', 'h', 'type', 'auto_scale', 'x_range', 'y_range'):
 
1029
                continue  # These are already parsed in create_chart_instance
 
1030
            attr_func = 'parse_' + attr
 
1031
            if not hasattr(self, attr_func):
 
1032
                warnings.warn('No parser for grammar attribute "%s"' % (attr))
 
1033
                continue
 
1034
            getattr(self, attr_func)(grammar[attr])
 
1035
 
 
1036
        return self.chart
 
1037
 
 
1038
    def parse_data(self, data):
 
1039
        self.chart.data = data
 
1040
 
 
1041
    @staticmethod
 
1042
    def get_possible_chart_types():
 
1043
        possible_charts = []
 
1044
        for cls_name in globals().keys():
 
1045
            if not cls_name.endswith('Chart'):
 
1046
                continue
 
1047
            cls = globals()[cls_name]
 
1048
            # Check if it is an abstract class
 
1049
            try:
 
1050
                a = cls(1, 1, auto_scale=False)
 
1051
                del a
 
1052
            except AbstractClassException:
 
1053
                continue
 
1054
            # Strip off "Class"
 
1055
            possible_charts.append(cls_name[:-5])
 
1056
        return possible_charts
 
1057
 
 
1058
    def create_chart_instance(self, grammar=None):
 
1059
        if not grammar:
 
1060
            grammar = self.grammar
 
1061
        assert(isinstance(grammar, dict))  # grammar must be a dict
 
1062
        assert('w' in grammar)  # width is required
 
1063
        assert('h' in grammar)  # height is required
 
1064
        assert('type' in grammar)  # type is required
 
1065
        chart_type = grammar['type']
 
1066
        w = grammar['w']
 
1067
        h = grammar['h']
 
1068
        auto_scale = grammar.get('auto_scale', None)
 
1069
        x_range = grammar.get('x_range', None)
 
1070
        y_range = grammar.get('y_range', None)
 
1071
        types = ChartGrammar.get_possible_chart_types()
 
1072
        if chart_type not in types:
 
1073
            raise UnknownChartType('%s is an unknown chart type. Possible '
 
1074
                'chart types are %s' % (chart_type, ','.join(types)))
 
1075
        return globals()[chart_type + 'Chart'](w, h, auto_scale=auto_scale,
 
1076
            x_range=x_range, y_range=y_range)
 
1077
 
 
1078
    def download(self):
 
1079
        pass
966
1080