/+junk/pygooglechart-py3k

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

« back to all changes in this revision

Viewing changes to pygooglechart.py

  • Committer: gak
  • Date: 2009-03-15 08:30:28 UTC
  • Revision ID: git-v1:33085bb9ee79265f2d97b0024c1b3bf33db09836
 - Version bump to 0.3.0
 - Fixed GPL date
 - Fixed line 80 overruns

Show diffs side-by-side

added added

removed removed

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