/+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 01:06:52 UTC
  • Revision ID: git-v1:2e9a4ccbb337ef7e85380191cd5c1c76efcd45f2
- pygooglechart.py converted to unix line breaks
- Added sparklines
- Minor code cleanup

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
26
 
import urllib.request, urllib.error
 
25
import urllib2
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.1.3'
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
 
    @classmethod
103
 
    def float_scale_value(cls, value, range):
104
 
        lower, upper = range
105
 
        assert(upper > lower)
106
 
        scaled = (value - lower) * (cls.max_value / (upper - lower))
107
 
        return scaled
108
 
 
109
 
    @classmethod
110
 
    def clip_value(cls, value):
111
 
        return max(0, min(value, cls.max_value))
112
 
 
113
 
    @classmethod
114
 
    def int_scale_value(cls, value, range):
115
 
        return int(round(cls.float_scale_value(value, range)))
116
 
 
117
 
    @classmethod
118
 
    def scale_value(cls, value, range):
119
 
        scaled = cls.int_scale_value(value, range)
120
 
        clipped = cls.clip_value(scaled)
121
 
        Data.check_clip(scaled, clipped)
122
 
        return clipped
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
82
 
131
83
class SimpleData(Data):
132
 
 
133
 
    max_value = 61
134
84
    enc_map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
135
85
 
136
86
    def __repr__(self):
 
87
        max_value = self.max_value()
137
88
        encoded_data = []
138
89
        for data in self.data:
139
90
            sub_data = []
140
91
            for value in data:
141
92
                if value is None:
142
93
                    sub_data.append('_')
143
 
                elif value >= 0 and value <= self.max_value:
 
94
                elif value >= 0 and value <= max_value:
144
95
                    sub_data.append(SimpleData.enc_map[value])
145
96
                else:
146
97
                    raise DataOutOfRangeException('cannot encode value: %d'
148
99
            encoded_data.append(''.join(sub_data))
149
100
        return 'chd=s:' + ','.join(encoded_data)
150
101
 
 
102
    @staticmethod
 
103
    def max_value():
 
104
        return 61
 
105
 
 
106
    @classmethod
 
107
    def scale_value(cls, value, range):
 
108
        lower, upper = range
 
109
        max_value = cls.max_value()
 
110
        scaled = int(round((float(value) - lower) * max_value / upper))
 
111
        clipped = max(0, min(scaled, max_value))
 
112
        return clipped
151
113
 
152
114
class TextData(Data):
153
115
 
154
 
    max_value = 100
155
 
 
156
116
    def __repr__(self):
 
117
        max_value = self.max_value()
157
118
        encoded_data = []
158
119
        for data in self.data:
159
120
            sub_data = []
160
121
            for value in data:
161
122
                if value is None:
162
123
                    sub_data.append(-1)
163
 
                elif value >= 0 and value <= self.max_value:
 
124
                elif value >= 0 and value <= max_value:
164
125
                    sub_data.append("%.1f" % float(value))
165
126
                else:
166
127
                    raise DataOutOfRangeException()
167
128
            encoded_data.append(','.join(sub_data))
168
 
        return 'chd=t:' + '%7c'.join(encoded_data)
 
129
        return 'chd=t:' + '|'.join(encoded_data)
 
130
 
 
131
    @staticmethod
 
132
    def max_value():
 
133
        return 100
169
134
 
170
135
    @classmethod
171
136
    def scale_value(cls, value, range):
172
 
        # use float values instead of integers because we don't need an encode
173
 
        # map index
174
 
        scaled = cls.float_scale_value(value, range)
175
 
        clipped = cls.clip_value(scaled)
176
 
        Data.check_clip(scaled, clipped)
 
137
        lower, upper = range
 
138
        max_value = cls.max_value()
 
139
        scaled = (float(value) - lower) * max_value / upper
 
140
        clipped = max(0, min(scaled, max_value))
177
141
        return clipped
178
142
 
179
 
 
180
143
class ExtendedData(Data):
181
 
 
182
 
    max_value = 4095
183
144
    enc_map = \
184
145
        'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-.'
185
146
 
186
147
    def __repr__(self):
 
148
        max_value = self.max_value()
187
149
        encoded_data = []
188
150
        enc_size = len(ExtendedData.enc_map)
189
151
        for data in self.data:
191
153
            for value in data:
192
154
                if value is None:
193
155
                    sub_data.append('__')
194
 
                elif value >= 0 and value <= self.max_value:
 
156
                elif value >= 0 and value <= max_value:
195
157
                    first, second = divmod(int(value), enc_size)
196
158
                    sub_data.append('%s%s' % (
197
159
                        ExtendedData.enc_map[first],
203
165
            encoded_data.append(''.join(sub_data))
204
166
        return 'chd=e:' + ','.join(encoded_data)
205
167
 
 
168
    @staticmethod
 
169
    def max_value():
 
170
        return 4095
 
171
 
 
172
    @classmethod
 
173
    def scale_value(cls, value, range):
 
174
        lower, upper = range
 
175
        max_value = cls.max_value()
 
176
        scaled = int(round((float(value) - lower) * max_value / upper))
 
177
        clipped = max(0, min(scaled, max_value))
 
178
        return clipped
 
179
 
206
180
 
207
181
# Axis Classes
208
182
# -----------------------------------------------------------------------------
209
183
 
210
184
 
211
185
class Axis(object):
212
 
 
213
186
    BOTTOM = 'x'
214
187
    TOP = 't'
215
188
    LEFT = 'y'
260
233
        self.values = [str(a) for a in values]
261
234
 
262
235
    def __repr__(self):
263
 
        return '%i:%%7c%s' % (self.axis_index, '%7c'.join(self.values))
 
236
        return '%i:|%s' % (self.axis_index, '|'.join(self.values))
264
237
 
265
238
 
266
239
class RangeAxis(Axis):
284
257
    of the chart. legend requires a list that corresponds to datasets.
285
258
    """
286
259
 
287
 
    BASE_URL = 'http://chart.apis.google.com/chart'
 
260
    BASE_URL = 'http://chart.apis.google.com/chart?'
288
261
    BACKGROUND = 'bg'
289
262
    CHART = 'c'
290
 
    ALPHA = 'a'
291
 
    VALID_SOLID_FILL_TYPES = (BACKGROUND, CHART, ALPHA)
292
263
    SOLID = 's'
293
264
    LINEAR_GRADIENT = 'lg'
294
265
    LINEAR_STRIPES = 'ls'
295
266
 
296
267
    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')
 
268
                 auto_scale=True, x_range=None, y_range=None):
 
269
        assert(type(self) != Chart)  # This is an abstract class
301
270
        assert(isinstance(width, int))
302
271
        assert(isinstance(height, int))
303
272
        self.width = width
304
273
        self.height = height
305
274
        self.data = []
306
275
        self.set_title(title)
307
 
        self.set_title_style(None, None)
308
276
        self.set_legend(legend)
309
 
        self.set_legend_position(None)
310
277
        self.set_colours(colours)
311
 
        self.set_colours_within_series(colours_within_series)
312
278
 
313
279
        # 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
 
280
        self.auto_scale = auto_scale    # Whether to automatically scale data
 
281
        self.x_range = x_range          # (min, max) x-axis range for scaling
 
282
        self.y_range = y_range          # (min, max) y-axis range for scaling
317
283
        self.scaled_data_class = None
318
284
        self.scaled_x_range = None
319
285
        self.scaled_y_range = None
321
287
        self.fill_types = {
322
288
            Chart.BACKGROUND: None,
323
289
            Chart.CHART: None,
324
 
            Chart.ALPHA: None,
325
290
        }
326
291
        self.fill_area = {
327
292
            Chart.BACKGROUND: None,
328
293
            Chart.CHART: None,
329
 
            Chart.ALPHA: None,
330
294
        }
331
295
        self.axis = []
332
296
        self.markers = []
333
 
        self.line_styles = {}
334
 
        self.grid = None
335
 
        self.title_colour = None
336
 
        self.title_font_size = None
337
297
 
338
298
    # URL generation
339
299
    # -------------------------------------------------------------------------
340
 
        
341
 
    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
 
        url_bits = self.get_url_bits(data_class=data_class)
346
 
        return '&'.join(url_bits)
347
 
 
348
 
    def get_url_bits(self, data_class=None):
 
300
 
 
301
    def get_url(self):
 
302
        url_bits = self.get_url_bits()
 
303
        return self.BASE_URL + '&'.join(url_bits)
 
304
 
 
305
    def get_url_bits(self):
349
306
        url_bits = []
350
307
        # required arguments
351
308
        url_bits.append(self.type_to_url())
352
309
        url_bits.append('chs=%ix%i' % (self.width, self.height))
353
 
        url_bits.append(self.data_to_url(data_class=data_class))
 
310
        url_bits.append(self.data_to_url())
354
311
        # optional arguments
355
312
        if self.title:
356
313
            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
314
        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))
 
315
            url_bits.append('chdl=%s' % '|'.join(self.legend))
364
316
        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))
 
317
            url_bits.append('chco=%s' % ','.join(self.colours))
368
318
        ret = self.fill_to_url()
369
319
        if ret:
370
320
            url_bits.append(ret)
371
321
        ret = self.axis_to_url()
372
322
        if ret:
373
 
            url_bits.append(ret)                    
 
323
            url_bits.append(ret)
374
324
        if self.markers:
375
 
            url_bits.append(self.markers_to_url())        
376
 
        if self.line_styles:
377
 
            style = []
378
 
            for index in range(max(self.line_styles) + 1):
379
 
                if index in self.line_styles:
380
 
                    values = self.line_styles[index]
381
 
                else:
382
 
                    values = ('1', )
383
 
                style.append(','.join(values))
384
 
            url_bits.append('chls=%s' % '%7c'.join(style))
385
 
        if self.grid:
386
 
            url_bits.append('chg=%s' % self.grid)
 
325
            url_bits.append(self.markers_to_url())
387
326
        return url_bits
388
327
 
389
328
    # Downloading
390
329
    # -------------------------------------------------------------------------
391
330
 
392
 
    def download(self, file_name, use_post=True):
393
 
        if use_post:
394
 
            opener = urllib.request.urlopen(self.BASE_URL, self.get_url_extension())
395
 
        else:
396
 
            opener = urllib.request.urlopen(self.get_url())
 
331
    def download(self, file_name):
 
332
        opener = urllib2.urlopen(self.get_url())
397
333
 
398
334
        if opener.headers['content-type'] != 'image/png':
399
335
            raise BadContentTypeException('Server responded with a ' \
400
336
                'content-type of %s' % opener.headers['content-type'])
401
337
 
402
 
        open(file_name, 'wb').write(opener.read())
 
338
        open(file_name, 'wb').write(urllib.urlopen(self.get_url()).read())
403
339
 
404
340
    # Simple settings
405
341
    # -------------------------------------------------------------------------
406
342
 
407
343
    def set_title(self, title):
408
344
        if title:
409
 
            self.title = urllib.parse.quote(title)
 
345
            self.title = urllib.quote(title)
410
346
        else:
411
347
            self.title = None
412
348
 
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
349
    def set_legend(self, legend):
422
350
        """legend needs to be a list, tuple or None"""
423
351
        assert(isinstance(legend, list) or isinstance(legend, tuple) or
424
352
            legend is None)
425
353
        if legend:
426
 
            self.legend = [urllib.parse.quote(a) for a in legend]
 
354
            self.legend = [urllib.quote(a) for a in legend]
427
355
        else:
428
356
            self.legend = None
429
357
 
430
 
    def set_legend_position(self, legend_position):
431
 
        if legend_position:
432
 
            self.legend_position = urllib.parse.quote(legend_position)
433
 
        else:    
434
 
            self.legend_position = None
435
 
 
436
358
    # Chart colours
437
359
    # -------------------------------------------------------------------------
438
360
 
446
368
                _check_colour(col)
447
369
        self.colours = colours
448
370
 
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
371
    # Background/Chart colours
460
372
    # -------------------------------------------------------------------------
461
373
 
462
374
    def fill_solid(self, area, colour):
463
 
        assert(area in Chart.VALID_SOLID_FILL_TYPES)
 
375
        assert(area in (Chart.BACKGROUND, Chart.CHART))
464
376
        _check_colour(colour)
465
377
        self.fill_area[area] = colour
466
378
        self.fill_types[area] = Chart.SOLID
470
382
        assert(angle >= 0 and angle <= 90)
471
383
        assert(len(args) % 2 == 0)
472
384
        args = list(args)  # args is probably a tuple and we need to mutate
473
 
        for a in range(int(len(args) / 2)):
 
385
        for a in xrange(len(args) / 2):
474
386
            col = args[a * 2]
475
387
            offset = args[a * 2 + 1]
476
388
            _check_colour(col)
479
391
        return args
480
392
 
481
393
    def fill_linear_gradient(self, area, angle, *args):
482
 
        assert(area in Chart.VALID_SOLID_FILL_TYPES)
 
394
        assert(area in (Chart.BACKGROUND, Chart.CHART))
483
395
        args = self._check_fill_linear(angle, *args)
484
396
        self.fill_types[area] = Chart.LINEAR_GRADIENT
485
397
        self.fill_area[area] = ','.join([str(angle)] + args)
486
398
 
487
399
    def fill_linear_stripes(self, area, angle, *args):
488
 
        assert(area in Chart.VALID_SOLID_FILL_TYPES)
 
400
        assert(area in (Chart.BACKGROUND, Chart.CHART))
489
401
        args = self._check_fill_linear(angle, *args)
490
402
        self.fill_types[area] = Chart.LINEAR_STRIPES
491
403
        self.fill_area[area] = ','.join([str(angle)] + args)
492
404
 
493
405
    def fill_to_url(self):
494
406
        areas = []
495
 
        for area in (Chart.BACKGROUND, Chart.CHART, Chart.ALPHA):
 
407
        for area in (Chart.BACKGROUND, Chart.CHART):
496
408
            if self.fill_types[area]:
497
409
                areas.append('%s,%s,%s' % (area, self.fill_types[area], \
498
410
                    self.fill_area[area]))
499
411
        if areas:
500
 
            return 'chf=' + '%7c'.join(areas)
 
412
            return 'chf=' + '|'.join(areas)
501
413
 
502
414
    # Data
503
415
    # -------------------------------------------------------------------------
518
430
            # simple encoding. I've found that this isn't sufficient,
519
431
            # e.g. examples/line-xy-circle.png. Let's try 100px.
520
432
            return SimpleData
 
433
        elif self.height < 500:
 
434
            return TextData
521
435
        else:
522
436
            return ExtendedData
523
437
 
524
 
    def _filter_none(self, data):
525
 
        return [r for r in data if r is not None]
526
 
 
527
438
    def data_x_range(self):
528
439
        """Return a 2-tuple giving the minimum and maximum x-axis
529
440
        data range.
530
441
        """
531
442
        try:
532
 
            lower = min([min(self._filter_none(s))
533
 
                         for type, s in self.annotated_data()
 
443
            lower = min([min(s) for type, s in self.annotated_data()
534
444
                         if type == 'x'])
535
 
            upper = max([max(self._filter_none(s))
536
 
                         for type, s in self.annotated_data()
 
445
            upper = max([max(s) for type, s in self.annotated_data()
537
446
                         if type == 'x'])
538
447
            return (lower, upper)
539
448
        except ValueError:
544
453
        data range.
545
454
        """
546
455
        try:
547
 
            lower = min([min(self._filter_none(s))
548
 
                         for type, s in self.annotated_data()
 
456
            lower = min([min(s) for type, s in self.annotated_data()
549
457
                         if type == 'y'])
550
 
            upper = max([max(self._filter_none(s)) + 1
551
 
                         for type, s in self.annotated_data()
 
458
            upper = max([max(s) for type, s in self.annotated_data()
552
459
                         if type == 'y'])
553
460
            return (lower, upper)
554
461
        except ValueError:
574
481
        if x_range is None:
575
482
            x_range = self.data_x_range()
576
483
            if x_range and x_range[0] > 0:
577
 
                x_range = (x_range[0], x_range[1])
 
484
                x_range = (0, x_range[1])
578
485
        self.scaled_x_range = x_range
579
486
 
580
487
        # Determine the y-axis range for scaling.
581
488
        if y_range is None:
582
489
            y_range = self.data_y_range()
583
490
            if y_range and y_range[0] > 0:
584
 
                y_range = (y_range[0], y_range[1])
 
491
                y_range = (0, y_range[1])
585
492
        self.scaled_y_range = y_range
586
493
 
587
494
        scaled_data = []
592
499
                scale_range = y_range
593
500
            elif type == 'marker-size':
594
501
                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)
 
502
            scaled_data.append([data_class.scale_value(v, scale_range)
 
503
                                for v in dataset])
603
504
        return scaled_data
604
505
 
605
506
    def add_data(self, data):
617
518
            data = self.data
618
519
        return repr(data_class(data))
619
520
 
620
 
    def annotated_data(self):
621
 
        for dataset in self.data:
622
 
            yield ('x', dataset)
623
 
 
624
521
    # Axis Labels
625
522
    # -------------------------------------------------------------------------
626
523
 
627
524
    def set_axis_labels(self, axis_type, values):
628
525
        assert(axis_type in Axis.TYPES)
629
 
        values = [urllib.parse.quote(str(a)) for a in values]
 
526
        values = [ urllib.quote(a) for a in values ]
630
527
        axis_index = len(self.axis)
631
528
        axis = LabelAxis(axis_index, axis_type, values)
632
529
        self.axis.append(axis)
676
573
        url_bits = []
677
574
        url_bits.append('chxt=%s' % ','.join(available_axis))
678
575
        if label_axis:
679
 
            url_bits.append('chxl=%s' % '%7c'.join(label_axis))
 
576
            url_bits.append('chxl=%s' % '|'.join(label_axis))
680
577
        if range_axis:
681
 
            url_bits.append('chxr=%s' % '%7c'.join(range_axis))
 
578
            url_bits.append('chxr=%s' % '|'.join(range_axis))
682
579
        if positions:
683
 
            url_bits.append('chxp=%s' % '%7c'.join(positions))
 
580
            url_bits.append('chxp=%s' % '|'.join(positions))
684
581
        if styles:
685
 
            url_bits.append('chxs=%s' % '%7c'.join(styles))
 
582
            url_bits.append('chxs=%s' % '|'.join(styles))
686
583
        return '&'.join(url_bits)
687
584
 
688
585
    # Markers, Ranges and Fill area (chm)
689
586
    # -------------------------------------------------------------------------
690
587
 
691
 
    def markers_to_url(self):        
692
 
        return 'chm=%s' % '%7c'.join([','.join(a) for a in self.markers])
 
588
    def markers_to_url(self):
 
589
        return 'chm=%s' % '|'.join([','.join(a) for a in self.markers])
693
590
 
694
 
    def add_marker(self, index, point, marker_type, colour, size, priority=0):
 
591
    def add_marker(self, index, point, marker_type, colour, size):
695
592
        self.markers.append((marker_type, colour, str(index), str(point), \
696
 
            str(size), str(priority)))
 
593
            str(size)))
697
594
 
698
595
    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)))        
 
596
        self.markers.append(('r', colour, '1', str(start), str(stop)))
709
597
 
710
598
    def add_vertical_range(self, colour, start, stop):
711
 
        self.markers.append(('R', colour, '0', str(start), str(stop)))
 
599
        self.markers.append(('R', colour, '1', str(start), str(stop)))
712
600
 
713
601
    def add_fill_range(self, colour, index_start, index_end):
714
602
        self.markers.append(('b', colour, str(index_start), str(index_end), \
717
605
    def add_fill_simple(self, colour):
718
606
        self.markers.append(('B', colour, '1', '1', '1'))
719
607
 
720
 
    # Line styles
721
 
    # -------------------------------------------------------------------------
 
608
 
 
609
class ScatterChart(Chart):
 
610
 
 
611
    def type_to_url(self):
 
612
        return 'cht=s'
 
613
 
 
614
    def annotated_data(self):
 
615
        yield ('x', self.data[0])
 
616
        yield ('y', self.data[1])
 
617
        if len(self.data) > 2:
 
618
            # The optional third dataset is relative sizing for point
 
619
            # markers.
 
620
            yield ('marker-size', self.data[2])
 
621
 
 
622
class LineChart(Chart):
 
623
 
 
624
    def __init__(self, *args, **kwargs):
 
625
        assert(type(self) != LineChart)  # This is an abstract class
 
626
        Chart.__init__(self, *args, **kwargs)
 
627
        self.line_styles = {}
 
628
        self.grid = None
722
629
 
723
630
    def set_line_style(self, index, thickness=1, line_segment=None, \
724
631
            blank_segment=None):
729
636
            value.append(str(blank_segment))
730
637
        self.line_styles[index] = value
731
638
 
732
 
    # Grid
733
 
    # -------------------------------------------------------------------------
734
 
 
735
639
    def set_grid(self, x_step, y_step, line_segment=1, \
736
640
            blank_segment=0):
737
641
        self.grid = '%s,%s,%s,%s' % (x_step, y_step, line_segment, \
738
642
            blank_segment)
739
643
 
740
 
 
741
 
class ScatterChart(Chart):
742
 
 
743
 
    def type_to_url(self):
744
 
        return 'cht=s'
745
 
 
746
 
    def annotated_data(self):
747
 
        yield ('x', self.data[0])
748
 
        yield ('y', self.data[1])
749
 
        if len(self.data) > 2:
750
 
            # The optional third dataset is relative sizing for point
751
 
            # markers.
752
 
            yield ('marker-size', self.data[2])
753
 
 
754
 
 
755
 
class LineChart(Chart):
756
 
 
757
 
    def __init__(self, *args, **kwargs):
758
 
        if type(self) == LineChart:
759
 
            raise AbstractClassException('This is an abstract class')
760
 
        Chart.__init__(self, *args, **kwargs)
 
644
    def get_url_bits(self):
 
645
        url_bits = Chart.get_url_bits(self)
 
646
        if self.line_styles:
 
647
            style = []
 
648
            # for index, values in self.line_style.items():
 
649
            for index in xrange(max(self.line_styles) + 1):
 
650
                if index in self.line_styles:
 
651
                    values = self.line_styles[index]
 
652
                else:
 
653
                    values = ('1', )
 
654
                style.append(','.join(values))
 
655
            url_bits.append('chls=%s' % '|'.join(style))
 
656
        if self.grid:
 
657
            url_bits.append('chg=%s' % self.grid)
 
658
        return url_bits
761
659
 
762
660
 
763
661
class SimpleLineChart(LineChart):
770
668
        for dataset in self.data:
771
669
            yield ('y', dataset)
772
670
 
773
 
 
774
671
class SparkLineChart(SimpleLineChart):
775
672
 
776
673
    def type_to_url(self):
777
674
        return 'cht=ls'
778
675
 
779
 
 
780
676
class XYLineChart(LineChart):
781
677
 
782
678
    def type_to_url(self):
790
686
            else:
791
687
                yield ('y', dataset)
792
688
 
793
 
 
794
689
class BarChart(Chart):
795
690
 
796
691
    def __init__(self, *args, **kwargs):
797
 
        if type(self) == BarChart:
798
 
            raise AbstractClassException('This is an abstract class')
 
692
        assert(type(self) != BarChart)  # This is an abstract class
799
693
        Chart.__init__(self, *args, **kwargs)
800
694
        self.bar_width = None
801
 
        self.zero_lines = {}
802
695
 
803
696
    def set_bar_width(self, bar_width):
804
697
        self.bar_width = bar_width
805
698
 
806
 
    def set_zero_line(self, index, zero_line):
807
 
        self.zero_lines[index] = zero_line
808
 
 
809
 
    def get_url_bits(self, data_class=None, skip_chbh=False):
810
 
        url_bits = Chart.get_url_bits(self, data_class=data_class)
811
 
        if not skip_chbh and self.bar_width is not None:
 
699
    def get_url_bits(self):
 
700
        url_bits = Chart.get_url_bits(self)
 
701
        if self.bar_width is not None:
812
702
            url_bits.append('chbh=%i' % self.bar_width)
813
 
        zero_line = []
814
 
        if self.zero_lines:
815
 
            for index in range(max(self.zero_lines) + 1):
816
 
                if index in self.zero_lines:
817
 
                    zero_line.append(str(self.zero_lines[index]))
818
 
                else:
819
 
                    zero_line.append('0')
820
 
            url_bits.append('chp=%s' % ','.join(zero_line))
821
703
        return url_bits
822
704
 
823
705
 
826
708
    def type_to_url(self):
827
709
        return 'cht=bhs'
828
710
 
 
711
    def annotated_data(self):
 
712
        for dataset in self.data:
 
713
            yield ('x', dataset)
829
714
 
830
715
class StackedVerticalBarChart(BarChart):
831
716
 
840
725
class GroupedBarChart(BarChart):
841
726
 
842
727
    def __init__(self, *args, **kwargs):
843
 
        if type(self) == GroupedBarChart:
844
 
            raise AbstractClassException('This is an abstract class')
 
728
        assert(type(self) != GroupedBarChart)  # This is an abstract class
845
729
        BarChart.__init__(self, *args, **kwargs)
846
730
        self.bar_spacing = None
847
731
        self.group_spacing = None
854
738
        """Set spacing between groups of bars."""
855
739
        self.group_spacing = spacing
856
740
 
857
 
    def get_url_bits(self, data_class=None):
 
741
    def get_url_bits(self):
858
742
        # Skip 'BarChart.get_url_bits' and call Chart directly so the parent
859
743
        # doesn't add "chbh" before we do.
860
 
        url_bits = BarChart.get_url_bits(self, data_class=data_class,
861
 
            skip_chbh=True)
 
744
        url_bits = Chart.get_url_bits(self)
862
745
        if self.group_spacing is not None:
863
746
            if self.bar_spacing is None:
864
 
                raise InvalidParametersException('Bar spacing is required ' \
865
 
                    'to be set when setting group spacing')
 
747
                raise InvalidParametersException('Bar spacing is required to ' \
 
748
                    'be set when setting group spacing')
866
749
            if self.bar_width is None:
867
750
                raise InvalidParametersException('Bar width is required to ' \
868
751
                    'be set when setting bar spacing')
873
756
                raise InvalidParametersException('Bar width is required to ' \
874
757
                    'be set when setting bar spacing')
875
758
            url_bits.append('chbh=%i,%i' % (self.bar_width, self.bar_spacing))
876
 
        elif self.bar_width:
 
759
        else:
877
760
            url_bits.append('chbh=%i' % self.bar_width)
878
761
        return url_bits
879
762
 
883
766
    def type_to_url(self):
884
767
        return 'cht=bhg'
885
768
 
 
769
    def annotated_data(self):
 
770
        for dataset in self.data:
 
771
            yield ('x', dataset)
 
772
 
886
773
 
887
774
class GroupedVerticalBarChart(GroupedBarChart):
888
775
 
897
784
class PieChart(Chart):
898
785
 
899
786
    def __init__(self, *args, **kwargs):
900
 
        if type(self) == PieChart:
901
 
            raise AbstractClassException('This is an abstract class')
 
787
        assert(type(self) != PieChart)  # This is an abstract class
902
788
        Chart.__init__(self, *args, **kwargs)
903
789
        self.pie_labels = []
904
 
        if self.y_range:
905
 
            warnings.warn('y_range is not used with %s.' % \
906
 
                (self.__class__.__name__))
907
790
 
908
791
    def set_pie_labels(self, labels):
909
 
        self.pie_labels = [urllib.parse.quote(a) for a in labels]
 
792
        self.pie_labels = [urllib.quote(a) for a in labels]
910
793
 
911
 
    def get_url_bits(self, data_class=None):
912
 
        url_bits = Chart.get_url_bits(self, data_class=data_class)
 
794
    def get_url_bits(self):
 
795
        url_bits = Chart.get_url_bits(self)
913
796
        if self.pie_labels:
914
 
            url_bits.append('chl=%s' % '%7c'.join(self.pie_labels))
 
797
            url_bits.append('chl=%s' % '|'.join(self.pie_labels))
915
798
        return url_bits
916
799
 
917
800
    def annotated_data(self):
918
801
        # Datasets are all y-axis data. However, there should only be
919
802
        # one dataset for pie charts.
920
803
        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)
 
804
            yield ('y', dataset)
927
805
 
928
806
 
929
807
class PieChart2D(PieChart):
948
826
            yield ('y', dataset)
949
827
 
950
828
 
951
 
class RadarChart(Chart):
952
 
 
953
 
    def type_to_url(self):
954
 
        return 'cht=r'
955
 
 
956
 
 
957
 
class SplineRadarChart(RadarChart):
958
 
 
959
 
    def type_to_url(self):
960
 
        return 'cht=rs'
961
 
 
962
 
 
963
 
class MapChart(Chart):
964
 
 
965
 
    def __init__(self, *args, **kwargs):
966
 
        Chart.__init__(self, *args, **kwargs)
967
 
        self.geo_area = 'world'
968
 
        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
 
        
996
 
    def type_to_url(self):
997
 
        return 'cht=t'
998
 
 
999
 
    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)
1035
 
 
1036
 
    def get_url_bits(self, data_class=None):
1037
 
        url_bits = Chart.get_url_bits(self, data_class=data_class)
1038
 
        url_bits.append('chtm=%s' % self.geo_area)
1039
 
        if self.codes:
1040
 
            url_bits.append('chld=%s' % ''.join(self.codes))
1041
 
        return url_bits
1042
 
 
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
 
 
1053
 
class GoogleOMeterChart(PieChart):
1054
 
    """Inheriting from PieChart because of similar labeling"""
1055
 
 
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
 
    def type_to_url(self):
1063
 
        return 'cht=gom'
1064
 
 
1065
 
 
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.parse.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
1161
 
 
 
829
def test():
 
830
    chart = GroupedVerticalBarChart(320, 200)
 
831
    chart = PieChart2D(320, 200)
 
832
    chart = ScatterChart(320, 200)
 
833
    chart = SimpleLineChart(320, 200)
 
834
    sine_data = [math.sin(float(a) / 10) * 2000 + 2000 for a in xrange(100)]
 
835
    random_data = [a * random.random() * 30 for a in xrange(40)]
 
836
    random_data2 = [random.random() * 4000 for a in xrange(10)]
 
837
#    chart.set_bar_width(50)
 
838
#    chart.set_bar_spacing(0)
 
839
    chart.add_data(sine_data)
 
840
    chart.add_data(random_data)
 
841
    chart.add_data(random_data2)
 
842
#    chart.set_line_style(1, thickness=2)
 
843
#    chart.set_line_style(2, line_segment=10, blank_segment=5)
 
844
#    chart.set_title('heloooo')
 
845
#    chart.set_legend(('sine wave', 'random * x'))
 
846
#    chart.set_colours(('ee2000', 'DDDDAA', 'fF03f2'))
 
847
#    chart.fill_solid(Chart.BACKGROUND, '123456')
 
848
#    chart.fill_linear_gradient(Chart.CHART, 20, '004070', 1, '300040', 0,
 
849
#        'aabbcc00', 0.5)
 
850
#    chart.fill_linear_stripes(Chart.CHART, 20, '204070', .2, '300040', .2,
 
851
#        'aabbcc00', 0.2)
 
852
    axis_left_index = chart.set_axis_range(Axis.LEFT, 0, 10)
 
853
    axis_left_index = chart.set_axis_range(Axis.LEFT, 0, 10)
 
854
    axis_left_index = chart.set_axis_range(Axis.LEFT, 0, 10)
 
855
    axis_right_index = chart.set_axis_range(Axis.RIGHT, 5, 30)
 
856
    axis_bottom_index = chart.set_axis_labels(Axis.BOTTOM, [1, 25, 95])
 
857
    chart.set_axis_positions(axis_bottom_index, [1, 25, 95])
 
858
    chart.set_axis_style(axis_bottom_index, '003050', 15)
 
859
 
 
860
#    chart.set_pie_labels(('apples', 'oranges', 'bananas'))
 
861
 
 
862
#    chart.set_grid(10, 10)
 
863
 
 
864
#    for a in xrange(0, 100, 10):
 
865
#        chart.add_marker(1, a, 'a', 'AACA20', 10)
 
866
 
 
867
    chart.add_horizontal_range('00A020', .2, .5)
 
868
    chart.add_vertical_range('00c030', .2, .4)
 
869
 
 
870
    chart.add_fill_simple('303030A0')
 
871
 
 
872
    chart.download('test.png')
 
873
 
 
874
    url = chart.get_url()
 
875
    print url
 
876
    if 0:
 
877
        data = urllib.urlopen(chart.get_url()).read()
 
878
        open('meh.png', 'wb').write(data)
 
879
        os.system('start meh.png')
 
880
 
 
881
 
 
882
if __name__ == '__main__':
 
883
    test()