/+junk/pygooglechart-py3k

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/%2Bjunk/pygooglechart-py3k

« back to all changes in this revision

Viewing changes to pygooglechart.py

  • Committer: gak
  • Date: 2008-04-26 04:49:19 UTC
  • Revision ID: git-v1:d473b3a692b4c5964a85ee59a58d3de0bb7adf32
Updating dist script to copy files to the server

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