/+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 03:55:42 UTC
  • Revision ID: git-v1:9ad3c7c5ee6741049e44364ec6e35b9676ff6539
Added map chart type

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):
287
277
    BASE_URL = 'http://chart.apis.google.com/chart?'
288
278
    BACKGROUND = 'bg'
289
279
    CHART = 'c'
290
 
    ALPHA = 'a'
291
 
    VALID_SOLID_FILL_TYPES = (BACKGROUND, CHART, ALPHA)
292
280
    SOLID = 's'
293
281
    LINEAR_GRADIENT = 'lg'
294
282
    LINEAR_STRIPES = 'ls'
295
283
 
296
284
    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')
 
285
                 auto_scale=True, x_range=None, y_range=None):
 
286
        assert(type(self) != Chart)  # This is an abstract class
301
287
        assert(isinstance(width, int))
302
288
        assert(isinstance(height, int))
303
289
        self.width = width
304
290
        self.height = height
305
291
        self.data = []
306
292
        self.set_title(title)
307
 
        self.set_title_style(None, None)
308
293
        self.set_legend(legend)
309
 
        self.set_legend_position(None)
310
294
        self.set_colours(colours)
311
 
        self.set_colours_within_series(colours_within_series)
312
295
 
313
296
        # 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
 
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
317
300
        self.scaled_data_class = None
318
301
        self.scaled_x_range = None
319
302
        self.scaled_y_range = None
321
304
        self.fill_types = {
322
305
            Chart.BACKGROUND: None,
323
306
            Chart.CHART: None,
324
 
            Chart.ALPHA: None,
325
307
        }
326
308
        self.fill_area = {
327
309
            Chart.BACKGROUND: None,
328
310
            Chart.CHART: None,
329
 
            Chart.ALPHA: None,
330
311
        }
331
312
        self.axis = []
332
313
        self.markers = []
349
330
        # optional arguments
350
331
        if self.title:
351
332
            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))
355
333
        if 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))
 
334
            url_bits.append('chdl=%s' % '|'.join(self.legend))
359
335
        if 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))
 
336
            url_bits.append('chco=%s' % ','.join(self.colours))
363
337
        ret = self.fill_to_url()
364
338
        if ret:
365
339
            url_bits.append(ret)
366
340
        ret = self.axis_to_url()
367
341
        if ret:
368
 
            url_bits.append(ret)                    
 
342
            url_bits.append(ret)
369
343
        if self.markers:
370
 
            url_bits.append(self.markers_to_url())        
 
344
            url_bits.append(self.markers_to_url())
371
345
        if self.line_styles:
372
346
            style = []
373
347
            for index in xrange(max(self.line_styles) + 1):
376
350
                else:
377
351
                    values = ('1', )
378
352
                style.append(','.join(values))
379
 
            url_bits.append('chls=%s' % '%7c'.join(style))
 
353
            url_bits.append('chls=%s' % '|'.join(style))
380
354
        if self.grid:
381
355
            url_bits.append('chg=%s' % self.grid)
382
356
        return url_bits
391
365
            raise BadContentTypeException('Server responded with a ' \
392
366
                'content-type of %s' % opener.headers['content-type'])
393
367
 
394
 
        open(file_name, 'wb').write(opener.read())
 
368
        open(file_name, 'wb').write(urllib.urlopen(self.get_url()).read())
395
369
 
396
370
    # Simple settings
397
371
    # -------------------------------------------------------------------------
402
376
        else:
403
377
            self.title = None
404
378
 
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
 
 
411
379
    def set_legend(self, legend):
412
380
        """legend needs to be a list, tuple or None"""
413
381
        assert(isinstance(legend, list) or isinstance(legend, tuple) or
417
385
        else:
418
386
            self.legend = None
419
387
 
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
 
 
426
388
    # Chart colours
427
389
    # -------------------------------------------------------------------------
428
390
 
436
398
                _check_colour(col)
437
399
        self.colours = colours
438
400
 
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
 
 
449
401
    # Background/Chart colours
450
402
    # -------------------------------------------------------------------------
451
403
 
452
404
    def fill_solid(self, area, colour):
453
 
        assert(area in Chart.VALID_SOLID_FILL_TYPES)
 
405
        assert(area in (Chart.BACKGROUND, Chart.CHART))
454
406
        _check_colour(colour)
455
407
        self.fill_area[area] = colour
456
408
        self.fill_types[area] = Chart.SOLID
460
412
        assert(angle >= 0 and angle <= 90)
461
413
        assert(len(args) % 2 == 0)
462
414
        args = list(args)  # args is probably a tuple and we need to mutate
463
 
        for a in xrange(int(len(args) / 2)):
 
415
        for a in xrange(len(args) / 2):
464
416
            col = args[a * 2]
465
417
            offset = args[a * 2 + 1]
466
418
            _check_colour(col)
469
421
        return args
470
422
 
471
423
    def fill_linear_gradient(self, area, angle, *args):
472
 
        assert(area in Chart.VALID_SOLID_FILL_TYPES)
 
424
        assert(area in (Chart.BACKGROUND, Chart.CHART))
473
425
        args = self._check_fill_linear(angle, *args)
474
426
        self.fill_types[area] = Chart.LINEAR_GRADIENT
475
427
        self.fill_area[area] = ','.join([str(angle)] + args)
476
428
 
477
429
    def fill_linear_stripes(self, area, angle, *args):
478
 
        assert(area in Chart.VALID_SOLID_FILL_TYPES)
 
430
        assert(area in (Chart.BACKGROUND, Chart.CHART))
479
431
        args = self._check_fill_linear(angle, *args)
480
432
        self.fill_types[area] = Chart.LINEAR_STRIPES
481
433
        self.fill_area[area] = ','.join([str(angle)] + args)
482
434
 
483
435
    def fill_to_url(self):
484
436
        areas = []
485
 
        for area in (Chart.BACKGROUND, Chart.CHART, Chart.ALPHA):
 
437
        for area in (Chart.BACKGROUND, Chart.CHART):
486
438
            if self.fill_types[area]:
487
439
                areas.append('%s,%s,%s' % (area, self.fill_types[area], \
488
440
                    self.fill_area[area]))
489
441
        if areas:
490
 
            return 'chf=' + '%7c'.join(areas)
 
442
            return 'chf=' + '|'.join(areas)
491
443
 
492
444
    # Data
493
445
    # -------------------------------------------------------------------------
511
463
        else:
512
464
            return ExtendedData
513
465
 
514
 
    def _filter_none(self, data):
515
 
        return [r for r in data if r is not None]
516
 
 
517
466
    def data_x_range(self):
518
467
        """Return a 2-tuple giving the minimum and maximum x-axis
519
468
        data range.
520
469
        """
521
470
        try:
522
 
            lower = min([min(self._filter_none(s))
523
 
                         for type, s in self.annotated_data()
 
471
            lower = min([min(s) for type, s in self.annotated_data()
524
472
                         if type == 'x'])
525
 
            upper = max([max(self._filter_none(s))
526
 
                         for type, s in self.annotated_data()
 
473
            upper = max([max(s) for type, s in self.annotated_data()
527
474
                         if type == 'x'])
528
475
            return (lower, upper)
529
476
        except ValueError:
534
481
        data range.
535
482
        """
536
483
        try:
537
 
            lower = min([min(self._filter_none(s))
538
 
                         for type, s in self.annotated_data()
 
484
            lower = min([min(s) for type, s in self.annotated_data()
539
485
                         if type == 'y'])
540
 
            upper = max([max(self._filter_none(s)) + 1
541
 
                         for type, s in self.annotated_data()
 
486
            upper = max([max(s) for type, s in self.annotated_data()
542
487
                         if type == 'y'])
543
488
            return (lower, upper)
544
489
        except ValueError:
564
509
        if x_range is None:
565
510
            x_range = self.data_x_range()
566
511
            if x_range and x_range[0] > 0:
567
 
                x_range = (x_range[0], x_range[1])
 
512
                x_range = (0, x_range[1])
568
513
        self.scaled_x_range = x_range
569
514
 
570
515
        # Determine the y-axis range for scaling.
571
516
        if y_range is None:
572
517
            y_range = self.data_y_range()
573
518
            if y_range and y_range[0] > 0:
574
 
                y_range = (y_range[0], y_range[1])
 
519
                y_range = (0, y_range[1])
575
520
        self.scaled_y_range = y_range
576
521
 
577
522
        scaled_data = []
582
527
                scale_range = y_range
583
528
            elif type == 'marker-size':
584
529
                scale_range = (0, max(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)
 
530
            scaled_data.append([data_class.scale_value(v, scale_range)
 
531
                                for v in dataset])
593
532
        return scaled_data
594
533
 
595
534
    def add_data(self, data):
607
546
            data = self.data
608
547
        return repr(data_class(data))
609
548
 
610
 
    def annotated_data(self):
611
 
        for dataset in self.data:
612
 
            yield ('x', dataset)
613
 
 
614
549
    # Axis Labels
615
550
    # -------------------------------------------------------------------------
616
551
 
617
552
    def set_axis_labels(self, axis_type, values):
618
553
        assert(axis_type in Axis.TYPES)
619
 
        values = [urllib.quote(str(a)) for a in values]
 
554
        values = [ urllib.quote(a) for a in values ]
620
555
        axis_index = len(self.axis)
621
556
        axis = LabelAxis(axis_index, axis_type, values)
622
557
        self.axis.append(axis)
666
601
        url_bits = []
667
602
        url_bits.append('chxt=%s' % ','.join(available_axis))
668
603
        if label_axis:
669
 
            url_bits.append('chxl=%s' % '%7c'.join(label_axis))
 
604
            url_bits.append('chxl=%s' % '|'.join(label_axis))
670
605
        if range_axis:
671
 
            url_bits.append('chxr=%s' % '%7c'.join(range_axis))
 
606
            url_bits.append('chxr=%s' % '|'.join(range_axis))
672
607
        if positions:
673
 
            url_bits.append('chxp=%s' % '%7c'.join(positions))
 
608
            url_bits.append('chxp=%s' % '|'.join(positions))
674
609
        if styles:
675
 
            url_bits.append('chxs=%s' % '%7c'.join(styles))
 
610
            url_bits.append('chxs=%s' % '|'.join(styles))
676
611
        return '&'.join(url_bits)
677
612
 
678
613
    # Markers, Ranges and Fill area (chm)
679
614
    # -------------------------------------------------------------------------
680
615
 
681
 
    def markers_to_url(self):        
682
 
        return 'chm=%s' % '%7c'.join([','.join(a) for a in self.markers])
 
616
    def markers_to_url(self):
 
617
        return 'chm=%s' % '|'.join([','.join(a) for a in self.markers])
683
618
 
684
 
    def add_marker(self, index, point, marker_type, colour, size, priority=0):
 
619
    def add_marker(self, index, point, marker_type, colour, size):
685
620
        self.markers.append((marker_type, colour, str(index), str(point), \
686
 
            str(size), str(priority)))
 
621
            str(size)))
687
622
 
688
623
    def add_horizontal_range(self, colour, start, 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)))        
 
624
        self.markers.append(('r', colour, '1', str(start), str(stop)))
699
625
 
700
626
    def add_vertical_range(self, colour, start, stop):
701
 
        self.markers.append(('R', colour, '0', str(start), str(stop)))
 
627
        self.markers.append(('R', colour, '1', str(start), str(stop)))
702
628
 
703
629
    def add_fill_range(self, colour, index_start, index_end):
704
630
        self.markers.append(('b', colour, str(index_start), str(index_end), \
741
667
            # markers.
742
668
            yield ('marker-size', self.data[2])
743
669
 
744
 
 
745
670
class LineChart(Chart):
746
671
 
747
672
    def __init__(self, *args, **kwargs):
748
 
        if type(self) == LineChart:
749
 
            raise AbstractClassException('This is an abstract class')
 
673
        assert(type(self) != LineChart)  # This is an abstract class
750
674
        Chart.__init__(self, *args, **kwargs)
751
675
 
 
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
 
752
680
 
753
681
class SimpleLineChart(LineChart):
754
682
 
760
688
        for dataset in self.data:
761
689
            yield ('y', dataset)
762
690
 
763
 
 
764
691
class SparkLineChart(SimpleLineChart):
765
692
 
766
693
    def type_to_url(self):
767
694
        return 'cht=ls'
768
695
 
769
 
 
770
696
class XYLineChart(LineChart):
771
697
 
772
698
    def type_to_url(self):
780
706
            else:
781
707
                yield ('y', dataset)
782
708
 
783
 
 
784
709
class BarChart(Chart):
785
710
 
786
711
    def __init__(self, *args, **kwargs):
787
 
        if type(self) == BarChart:
788
 
            raise AbstractClassException('This is an abstract class')
 
712
        assert(type(self) != BarChart)  # This is an abstract class
789
713
        Chart.__init__(self, *args, **kwargs)
790
714
        self.bar_width = None
791
 
        self.zero_lines = {}
792
715
 
793
716
    def set_bar_width(self, bar_width):
794
717
        self.bar_width = bar_width
795
718
 
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):
 
719
    def get_url_bits(self, data_class=None):
800
720
        url_bits = Chart.get_url_bits(self, data_class=data_class)
801
 
        if not skip_chbh and self.bar_width is not None:
 
721
        if self.bar_width is not None:
802
722
            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))
811
723
        return url_bits
812
724
 
813
725
 
816
728
    def type_to_url(self):
817
729
        return 'cht=bhs'
818
730
 
 
731
    def annotated_data(self):
 
732
        for dataset in self.data:
 
733
            yield ('x', dataset)
819
734
 
820
735
class StackedVerticalBarChart(BarChart):
821
736
 
830
745
class GroupedBarChart(BarChart):
831
746
 
832
747
    def __init__(self, *args, **kwargs):
833
 
        if type(self) == GroupedBarChart:
834
 
            raise AbstractClassException('This is an abstract class')
 
748
        assert(type(self) != GroupedBarChart)  # This is an abstract class
835
749
        BarChart.__init__(self, *args, **kwargs)
836
750
        self.bar_spacing = None
837
751
        self.group_spacing = None
847
761
    def get_url_bits(self, data_class=None):
848
762
        # Skip 'BarChart.get_url_bits' and call Chart directly so the parent
849
763
        # doesn't add "chbh" before we do.
850
 
        url_bits = BarChart.get_url_bits(self, data_class=data_class,
851
 
            skip_chbh=True)
 
764
        url_bits = Chart.get_url_bits(self, data_class=data_class)
852
765
        if self.group_spacing is not None:
853
766
            if self.bar_spacing is None:
854
 
                raise InvalidParametersException('Bar spacing is required ' \
855
 
                    'to be set when setting group spacing')
 
767
                raise InvalidParametersException('Bar spacing is required to ' \
 
768
                    'be set when setting group spacing')
856
769
            if self.bar_width is None:
857
770
                raise InvalidParametersException('Bar width is required to ' \
858
771
                    'be set when setting bar spacing')
873
786
    def type_to_url(self):
874
787
        return 'cht=bhg'
875
788
 
 
789
    def annotated_data(self):
 
790
        for dataset in self.data:
 
791
            yield ('x', dataset)
 
792
 
876
793
 
877
794
class GroupedVerticalBarChart(GroupedBarChart):
878
795
 
887
804
class PieChart(Chart):
888
805
 
889
806
    def __init__(self, *args, **kwargs):
890
 
        if type(self) == PieChart:
891
 
            raise AbstractClassException('This is an abstract class')
 
807
        assert(type(self) != PieChart)  # This is an abstract class
892
808
        Chart.__init__(self, *args, **kwargs)
893
809
        self.pie_labels = []
894
 
        if self.y_range:
895
 
            warnings.warn('y_range is not used with %s.' % \
896
 
                (self.__class__.__name__))
897
810
 
898
811
    def set_pie_labels(self, labels):
899
812
        self.pie_labels = [urllib.quote(a) for a in labels]
901
814
    def get_url_bits(self, data_class=None):
902
815
        url_bits = Chart.get_url_bits(self, data_class=data_class)
903
816
        if self.pie_labels:
904
 
            url_bits.append('chl=%s' % '%7c'.join(self.pie_labels))
 
817
            url_bits.append('chl=%s' % '|'.join(self.pie_labels))
905
818
        return url_bits
906
819
 
907
820
    def annotated_data(self):
908
821
        # Datasets are all y-axis data. However, there should only be
909
822
        # one dataset for pie charts.
910
823
        for dataset in self.data:
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)
 
824
            yield ('y', dataset)
917
825
 
918
826
 
919
827
class PieChart2D(PieChart):
943
851
    def type_to_url(self):
944
852
        return 'cht=r'
945
853
 
 
854
    def annotated_data(self):
 
855
        for dataset in self.data:
 
856
            yield ('x', dataset)
946
857
 
947
858
class SplineRadarChart(RadarChart):
948
859
 
956
867
        Chart.__init__(self, *args, **kwargs)
957
868
        self.geo_area = 'world'
958
869
        self.codes = []
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
 
        
 
870
 
986
871
    def type_to_url(self):
987
872
        return 'cht=t'
988
873
 
989
874
    def set_codes(self, 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)
 
875
        self.codes = codes
1025
876
 
1026
877
    def get_url_bits(self, data_class=None):
1027
878
        url_bits = Chart.get_url_bits(self, data_class=data_class)
1030
881
            url_bits.append('chld=%s' % ''.join(self.codes))
1031
882
        return url_bits
1032
883
 
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
 
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()
1151
946