/+junk/pygooglechart-py3k

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/%2Bjunk/pygooglechart-py3k
22 by gak
- pygooglechart.py converted to unix line breaks
1
"""
35 by gak
- Initial "grammar" code
2
pygooglechart - A complete Python wrapper for the Google Chart API
22 by gak
- pygooglechart.py converted to unix line breaks
3
4
http://pygooglechart.slowchop.com/
5
58 by gak
- Version bump to 0.3.0
6
Copyright 2007-2009 Gerald Kaszuba
22 by gak
- pygooglechart.py converted to unix line breaks
7
8
This program is free software: you can redistribute it and/or modify
9
it under the terms of the GNU General Public License as published by
10
the Free Software Foundation, either version 3 of the License, or
11
(at your option) any later version.
12
13
This program is distributed in the hope that it will be useful,
14
but WITHOUT ANY WARRANTY; without even the implied warranty of
15
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
GNU General Public License for more details.
17
18
You should have received a copy of the GNU General Public License
19
along with this program.  If not, see <http://www.gnu.org/licenses/>.
20
21
"""
52 by gak
- Few fixes for python 3.0
22
from __future__ import division
22 by gak
- pygooglechart.py converted to unix line breaks
23
24
import os
25
import urllib
68 by Gustav Hartvigsson
Made it compatible with Python 3.1
26
import urllib.request, urllib.error
22 by gak
- pygooglechart.py converted to unix line breaks
27
import math
28
import random
29
import re
36 by gak
- Really added initial unit tests
30
import warnings
31
import copy
22 by gak
- pygooglechart.py converted to unix line breaks
32
33
# Helper variables and functions
34
# -----------------------------------------------------------------------------
35
58 by gak
- Version bump to 0.3.0
36
__version__ = '0.3.0'
35 by gak
- Initial "grammar" code
37
__author__ = 'Gerald Kaszuba'
22 by gak
- pygooglechart.py converted to unix line breaks
38
39
reo_colour = re.compile('^([A-Fa-f0-9]{2,2}){3,4}$')
40
41
def _check_colour(colour):
42
    if not reo_colour.match(colour):
43
        raise InvalidParametersException('Colours need to be in ' \
44
            'RRGGBB or RRGGBBAA format. One of your colours has %s' % \
45
            colour)
46
36 by gak
- Really added initial unit tests
47
48
def _reset_warnings():
49
    """Helper function to reset all warnings. Used by the unit tests."""
50
    globals()['__warningregistry__'] = None
51
52
22 by gak
- pygooglechart.py converted to unix line breaks
53
# Exception Classes
54
# -----------------------------------------------------------------------------
55
56
57
class PyGoogleChartException(Exception):
58
    pass
59
60
61
class DataOutOfRangeException(PyGoogleChartException):
62
    pass
63
64
65
class UnknownDataTypeException(PyGoogleChartException):
66
    pass
67
68
69
class NoDataGivenException(PyGoogleChartException):
70
    pass
71
72
73
class InvalidParametersException(PyGoogleChartException):
74
    pass
75
76
77
class BadContentTypeException(PyGoogleChartException):
78
    pass
79
80
35 by gak
- Initial "grammar" code
81
class AbstractClassException(PyGoogleChartException):
82
    pass
83
84
85
class UnknownChartType(PyGoogleChartException):
86
    pass
87
57 by gak
- #23 MapChart extensions (thanks to Andreas Schawo)
88
class UnknownCountryCodeException(PyGoogleChartException):
89
    pass
35 by gak
- Initial "grammar" code
90
22 by gak
- pygooglechart.py converted to unix line breaks
91
# Data Classes
92
# -----------------------------------------------------------------------------
93
94
95
class Data(object):
96
97
    def __init__(self, data):
35 by gak
- Initial "grammar" code
98
        if type(self) == Data:
99
            raise AbstractClassException('This is an abstract class')
22 by gak
- pygooglechart.py converted to unix line breaks
100
        self.data = data
101
25 by gak
Autoscale fixes and refactoring by Grahm Ullrich
102
    @classmethod
103
    def float_scale_value(cls, value, range):
104
        lower, upper = range
34 by gak
- initial unit tests
105
        assert(upper > lower)
52 by gak
- Few fixes for python 3.0
106
        scaled = (value - lower) * (cls.max_value / (upper - lower))
25 by gak
Autoscale fixes and refactoring by Grahm Ullrich
107
        return scaled
108
109
    @classmethod
110
    def clip_value(cls, value):
36 by gak
- Really added initial unit tests
111
        return max(0, min(value, cls.max_value))
25 by gak
Autoscale fixes and refactoring by Grahm Ullrich
112
113
    @classmethod
114
    def int_scale_value(cls, value, range):
34 by gak
- initial unit tests
115
        return int(round(cls.float_scale_value(value, range)))
25 by gak
Autoscale fixes and refactoring by Grahm Ullrich
116
117
    @classmethod
118
    def scale_value(cls, value, range):
119
        scaled = cls.int_scale_value(value, range)
120
        clipped = cls.clip_value(scaled)
37 by gak
- Added "colours within series" option to chart. (chco=xxx|xxx) (Steve Brandt)
121
        Data.check_clip(scaled, clipped)
122
        return clipped
123
124
    @staticmethod
125
    def check_clip(scaled, clipped):
36 by gak
- Really added initial unit tests
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.')
22 by gak
- pygooglechart.py converted to unix line breaks
129
33 by gak
pep8 fixes, version bump
130
22 by gak
- pygooglechart.py converted to unix line breaks
131
class SimpleData(Data):
33 by gak
pep8 fixes, version bump
132
36 by gak
- Really added initial unit tests
133
    max_value = 61
22 by gak
- pygooglechart.py converted to unix line breaks
134
    enc_map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
135
136
    def __repr__(self):
137
        encoded_data = []
138
        for data in self.data:
139
            sub_data = []
140
            for value in data:
141
                if value is None:
142
                    sub_data.append('_')
36 by gak
- Really added initial unit tests
143
                elif value >= 0 and value <= self.max_value:
22 by gak
- pygooglechart.py converted to unix line breaks
144
                    sub_data.append(SimpleData.enc_map[value])
145
                else:
146
                    raise DataOutOfRangeException('cannot encode value: %d'
147
                                                  % value)
148
            encoded_data.append(''.join(sub_data))
149
        return 'chd=s:' + ','.join(encoded_data)
150
33 by gak
pep8 fixes, version bump
151
22 by gak
- pygooglechart.py converted to unix line breaks
152
class TextData(Data):
153
36 by gak
- Really added initial unit tests
154
    max_value = 100
155
22 by gak
- pygooglechart.py converted to unix line breaks
156
    def __repr__(self):
157
        encoded_data = []
158
        for data in self.data:
159
            sub_data = []
160
            for value in data:
161
                if value is None:
162
                    sub_data.append(-1)
36 by gak
- Really added initial unit tests
163
                elif value >= 0 and value <= self.max_value:
22 by gak
- pygooglechart.py converted to unix line breaks
164
                    sub_data.append("%.1f" % float(value))
165
                else:
166
                    raise DataOutOfRangeException()
167
            encoded_data.append(','.join(sub_data))
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
168
        return 'chd=t:' + '%7c'.join(encoded_data)
22 by gak
- pygooglechart.py converted to unix line breaks
169
170
    @classmethod
171
    def scale_value(cls, value, range):
25 by gak
Autoscale fixes and refactoring by Grahm Ullrich
172
        # use float values instead of integers because we don't need an encode
173
        # map index
33 by gak
pep8 fixes, version bump
174
        scaled = cls.float_scale_value(value, range)
25 by gak
Autoscale fixes and refactoring by Grahm Ullrich
175
        clipped = cls.clip_value(scaled)
37 by gak
- Added "colours within series" option to chart. (chco=xxx|xxx) (Steve Brandt)
176
        Data.check_clip(scaled, clipped)
25 by gak
Autoscale fixes and refactoring by Grahm Ullrich
177
        return clipped
22 by gak
- pygooglechart.py converted to unix line breaks
178
33 by gak
pep8 fixes, version bump
179
22 by gak
- pygooglechart.py converted to unix line breaks
180
class ExtendedData(Data):
34 by gak
- initial unit tests
181
36 by gak
- Really added initial unit tests
182
    max_value = 4095
22 by gak
- pygooglechart.py converted to unix line breaks
183
    enc_map = \
184
        'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-.'
185
186
    def __repr__(self):
187
        encoded_data = []
188
        enc_size = len(ExtendedData.enc_map)
189
        for data in self.data:
190
            sub_data = []
191
            for value in data:
192
                if value is None:
193
                    sub_data.append('__')
36 by gak
- Really added initial unit tests
194
                elif value >= 0 and value <= self.max_value:
22 by gak
- pygooglechart.py converted to unix line breaks
195
                    first, second = divmod(int(value), enc_size)
196
                    sub_data.append('%s%s' % (
197
                        ExtendedData.enc_map[first],
198
                        ExtendedData.enc_map[second]))
199
                else:
200
                    raise DataOutOfRangeException( \
201
                        'Item #%i "%s" is out of range' % (data.index(value), \
202
                        value))
203
            encoded_data.append(''.join(sub_data))
204
        return 'chd=e:' + ','.join(encoded_data)
205
206
207
# Axis Classes
208
# -----------------------------------------------------------------------------
209
210
211
class Axis(object):
34 by gak
- initial unit tests
212
22 by gak
- pygooglechart.py converted to unix line breaks
213
    BOTTOM = 'x'
214
    TOP = 't'
215
    LEFT = 'y'
216
    RIGHT = 'r'
217
    TYPES = (BOTTOM, TOP, LEFT, RIGHT)
218
219
    def __init__(self, axis_index, axis_type, **kw):
220
        assert(axis_type in Axis.TYPES)
221
        self.has_style = False
222
        self.axis_index = axis_index
223
        self.axis_type = axis_type
224
        self.positions = None
225
226
    def set_index(self, axis_index):
227
        self.axis_index = axis_index
228
229
    def set_positions(self, positions):
230
        self.positions = positions
231
232
    def set_style(self, colour, font_size=None, alignment=None):
233
        _check_colour(colour)
234
        self.colour = colour
235
        self.font_size = font_size
236
        self.alignment = alignment
237
        self.has_style = True
238
239
    def style_to_url(self):
240
        bits = []
241
        bits.append(str(self.axis_index))
242
        bits.append(self.colour)
243
        if self.font_size is not None:
244
            bits.append(str(self.font_size))
245
            if self.alignment is not None:
246
                bits.append(str(self.alignment))
247
        return ','.join(bits)
248
249
    def positions_to_url(self):
250
        bits = []
251
        bits.append(str(self.axis_index))
252
        bits += [str(a) for a in self.positions]
253
        return ','.join(bits)
254
255
256
class LabelAxis(Axis):
257
258
    def __init__(self, axis_index, axis_type, values, **kwargs):
259
        Axis.__init__(self, axis_index, axis_type, **kwargs)
260
        self.values = [str(a) for a in values]
261
262
    def __repr__(self):
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
263
        return '%i:%%7c%s' % (self.axis_index, '%7c'.join(self.values))
22 by gak
- pygooglechart.py converted to unix line breaks
264
265
266
class RangeAxis(Axis):
267
268
    def __init__(self, axis_index, axis_type, low, high, **kwargs):
269
        Axis.__init__(self, axis_index, axis_type, **kwargs)
270
        self.low = low
271
        self.high = high
272
273
    def __repr__(self):
274
        return '%i,%s,%s' % (self.axis_index, self.low, self.high)
275
276
# Chart Classes
277
# -----------------------------------------------------------------------------
278
279
280
class Chart(object):
281
    """Abstract class for all chart types.
282
283
    width are height specify the dimensions of the image. title sets the title
284
    of the chart. legend requires a list that corresponds to datasets.
285
    """
286
62 by Stefan Hacker
POST requests. Closes #1
287
    BASE_URL = 'http://chart.apis.google.com/chart'
22 by gak
- pygooglechart.py converted to unix line breaks
288
    BACKGROUND = 'bg'
289
    CHART = 'c'
30 by gak
Added zero line to bar charts
290
    ALPHA = 'a'
291
    VALID_SOLID_FILL_TYPES = (BACKGROUND, CHART, ALPHA)
22 by gak
- pygooglechart.py converted to unix line breaks
292
    SOLID = 's'
293
    LINEAR_GRADIENT = 'lg'
294
    LINEAR_STRIPES = 'ls'
295
296
    def __init__(self, width, height, title=None, legend=None, colours=None,
37 by gak
- Added "colours within series" option to chart. (chco=xxx|xxx) (Steve Brandt)
297
            auto_scale=True, x_range=None, y_range=None,
298
            colours_within_series=None):
35 by gak
- Initial "grammar" code
299
        if type(self) == Chart:
300
            raise AbstractClassException('This is an abstract class')
22 by gak
- pygooglechart.py converted to unix line breaks
301
        assert(isinstance(width, int))
302
        assert(isinstance(height, int))
303
        self.width = width
304
        self.height = height
305
        self.data = []
306
        self.set_title(title)
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
307
        self.set_title_style(None, None)
22 by gak
- pygooglechart.py converted to unix line breaks
308
        self.set_legend(legend)
37 by gak
- Added "colours within series" option to chart. (chco=xxx|xxx) (Steve Brandt)
309
        self.set_legend_position(None)
22 by gak
- pygooglechart.py converted to unix line breaks
310
        self.set_colours(colours)
37 by gak
- Added "colours within series" option to chart. (chco=xxx|xxx) (Steve Brandt)
311
        self.set_colours_within_series(colours_within_series)
22 by gak
- pygooglechart.py converted to unix line breaks
312
313
        # Data for scaling.
36 by gak
- Really added initial unit tests
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
22 by gak
- pygooglechart.py converted to unix line breaks
317
        self.scaled_data_class = None
318
        self.scaled_x_range = None
319
        self.scaled_y_range = None
320
321
        self.fill_types = {
322
            Chart.BACKGROUND: None,
323
            Chart.CHART: None,
30 by gak
Added zero line to bar charts
324
            Chart.ALPHA: None,
22 by gak
- pygooglechart.py converted to unix line breaks
325
        }
326
        self.fill_area = {
327
            Chart.BACKGROUND: None,
328
            Chart.CHART: None,
30 by gak
Added zero line to bar charts
329
            Chart.ALPHA: None,
22 by gak
- pygooglechart.py converted to unix line breaks
330
        }
331
        self.axis = []
332
        self.markers = []
27 by gak
grids and line styles are not only restricted to line chart types
333
        self.line_styles = {}
334
        self.grid = None
60 by gak
- #16 Allow either arguments in set_title_style()
335
        self.title_colour = None
336
        self.title_font_size = None
22 by gak
- pygooglechart.py converted to unix line breaks
337
338
    # URL generation
63 by Gerald Kaszuba
The patch for the previous commit was applied incorrectly. Fixed.
339
    # -------------------------------------------------------------------------
62 by Stefan Hacker
POST requests. Closes #1
340
        
63 by Gerald Kaszuba
The patch for the previous commit was applied incorrectly. Fixed.
341
    def get_url(self, data_class=None):
62 by Stefan Hacker
POST requests. Closes #1
342
        return self.BASE_URL + '?' + self.get_url_extension(data_class)
343
    
63 by Gerald Kaszuba
The patch for the previous commit was applied incorrectly. Fixed.
344
    def get_url_extension(self, data_class=None):
25 by gak
Autoscale fixes and refactoring by Grahm Ullrich
345
        url_bits = self.get_url_bits(data_class=data_class)
62 by Stefan Hacker
POST requests. Closes #1
346
        return '&'.join(url_bits)
22 by gak
- pygooglechart.py converted to unix line breaks
347
25 by gak
Autoscale fixes and refactoring by Grahm Ullrich
348
    def get_url_bits(self, data_class=None):
22 by gak
- pygooglechart.py converted to unix line breaks
349
        url_bits = []
350
        # required arguments
351
        url_bits.append(self.type_to_url())
352
        url_bits.append('chs=%ix%i' % (self.width, self.height))
25 by gak
Autoscale fixes and refactoring by Grahm Ullrich
353
        url_bits.append(self.data_to_url(data_class=data_class))
22 by gak
- pygooglechart.py converted to unix line breaks
354
        # optional arguments
355
        if self.title:
356
            url_bits.append('chtt=%s' % self.title)
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
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))
22 by gak
- pygooglechart.py converted to unix line breaks
360
        if self.legend:
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
361
            url_bits.append('chdl=%s' % '%7c'.join(self.legend))
37 by gak
- Added "colours within series" option to chart. (chco=xxx|xxx) (Steve Brandt)
362
        if self.legend_position:
363
            url_bits.append('chdlp=%s' % (self.legend_position))
22 by gak
- pygooglechart.py converted to unix line breaks
364
        if self.colours:
37 by gak
- Added "colours within series" option to chart. (chco=xxx|xxx) (Steve Brandt)
365
            url_bits.append('chco=%s' % ','.join(self.colours))            
366
        if self.colours_within_series:
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
367
            url_bits.append('chco=%s' % '%7c'.join(self.colours_within_series))
22 by gak
- pygooglechart.py converted to unix line breaks
368
        ret = self.fill_to_url()
369
        if ret:
370
            url_bits.append(ret)
371
        ret = self.axis_to_url()
372
        if ret:
37 by gak
- Added "colours within series" option to chart. (chco=xxx|xxx) (Steve Brandt)
373
            url_bits.append(ret)                    
22 by gak
- pygooglechart.py converted to unix line breaks
374
        if self.markers:
37 by gak
- Added "colours within series" option to chart. (chco=xxx|xxx) (Steve Brandt)
375
            url_bits.append(self.markers_to_url())        
27 by gak
grids and line styles are not only restricted to line chart types
376
        if self.line_styles:
377
            style = []
68 by Gustav Hartvigsson
Made it compatible with Python 3.1
378
            for index in range(max(self.line_styles) + 1):
27 by gak
grids and line styles are not only restricted to line chart types
379
                if index in self.line_styles:
380
                    values = self.line_styles[index]
381
                else:
382
                    values = ('1', )
383
                style.append(','.join(values))
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
384
            url_bits.append('chls=%s' % '%7c'.join(style))
27 by gak
grids and line styles are not only restricted to line chart types
385
        if self.grid:
386
            url_bits.append('chg=%s' % self.grid)
22 by gak
- pygooglechart.py converted to unix line breaks
387
        return url_bits
388
389
    # Downloading
390
    # -------------------------------------------------------------------------
391
63 by Gerald Kaszuba
The patch for the previous commit was applied incorrectly. Fixed.
392
    def download(self, file_name, use_post=True):
62 by Stefan Hacker
POST requests. Closes #1
393
        if use_post:
68 by Gustav Hartvigsson
Made it compatible with Python 3.1
394
            opener = urllib.request.urlopen(self.BASE_URL, self.get_url_extension())
62 by Stefan Hacker
POST requests. Closes #1
395
        else:
68 by Gustav Hartvigsson
Made it compatible with Python 3.1
396
            opener = urllib.request.urlopen(self.get_url())
22 by gak
- pygooglechart.py converted to unix line breaks
397
398
        if opener.headers['content-type'] != 'image/png':
399
            raise BadContentTypeException('Server responded with a ' \
400
                'content-type of %s' % opener.headers['content-type'])
401
41 by gak
Fixed bug where the module would download twice (#7) (Evan Lezar)
402
        open(file_name, 'wb').write(opener.read())
22 by gak
- pygooglechart.py converted to unix line breaks
403
404
    # Simple settings
405
    # -------------------------------------------------------------------------
406
407
    def set_title(self, title):
408
        if title:
68 by Gustav Hartvigsson
Made it compatible with Python 3.1
409
            self.title = urllib.parse.quote(title)
22 by gak
- pygooglechart.py converted to unix line breaks
410
        else:
411
            self.title = None
412
60 by gak
- #16 Allow either arguments in set_title_style()
413
    def set_title_style(self, colour=None, font_size=None):
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
414
        if not colour is None:
415
            _check_colour(colour)
60 by gak
- #16 Allow either arguments in set_title_style()
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
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
420
22 by gak
- pygooglechart.py converted to unix line breaks
421
    def set_legend(self, legend):
422
        """legend needs to be a list, tuple or None"""
423
        assert(isinstance(legend, list) or isinstance(legend, tuple) or
424
            legend is None)
425
        if legend:
68 by Gustav Hartvigsson
Made it compatible with Python 3.1
426
            self.legend = [urllib.parse.quote(a) for a in legend]
22 by gak
- pygooglechart.py converted to unix line breaks
427
        else:
428
            self.legend = None
429
37 by gak
- Added "colours within series" option to chart. (chco=xxx|xxx) (Steve Brandt)
430
    def set_legend_position(self, legend_position):
431
        if legend_position:
68 by Gustav Hartvigsson
Made it compatible with Python 3.1
432
            self.legend_position = urllib.parse.quote(legend_position)
37 by gak
- Added "colours within series" option to chart. (chco=xxx|xxx) (Steve Brandt)
433
        else:    
434
            self.legend_position = None
435
22 by gak
- pygooglechart.py converted to unix line breaks
436
    # Chart colours
437
    # -------------------------------------------------------------------------
438
439
    def set_colours(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 = colours
448
37 by gak
- Added "colours within series" option to chart. (chco=xxx|xxx) (Steve Brandt)
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
22 by gak
- pygooglechart.py converted to unix line breaks
459
    # Background/Chart colours
460
    # -------------------------------------------------------------------------
461
462
    def fill_solid(self, area, colour):
30 by gak
Added zero line to bar charts
463
        assert(area in Chart.VALID_SOLID_FILL_TYPES)
22 by gak
- pygooglechart.py converted to unix line breaks
464
        _check_colour(colour)
465
        self.fill_area[area] = colour
466
        self.fill_types[area] = Chart.SOLID
467
468
    def _check_fill_linear(self, angle, *args):
469
        assert(isinstance(args, list) or isinstance(args, tuple))
470
        assert(angle >= 0 and angle <= 90)
471
        assert(len(args) % 2 == 0)
472
        args = list(args)  # args is probably a tuple and we need to mutate
68 by Gustav Hartvigsson
Made it compatible with Python 3.1
473
        for a in range(int(len(args) / 2)):
22 by gak
- pygooglechart.py converted to unix line breaks
474
            col = args[a * 2]
475
            offset = args[a * 2 + 1]
476
            _check_colour(col)
477
            assert(offset >= 0 and offset <= 1)
478
            args[a * 2 + 1] = str(args[a * 2 + 1])
479
        return args
480
481
    def fill_linear_gradient(self, area, angle, *args):
30 by gak
Added zero line to bar charts
482
        assert(area in Chart.VALID_SOLID_FILL_TYPES)
22 by gak
- pygooglechart.py converted to unix line breaks
483
        args = self._check_fill_linear(angle, *args)
484
        self.fill_types[area] = Chart.LINEAR_GRADIENT
485
        self.fill_area[area] = ','.join([str(angle)] + args)
486
487
    def fill_linear_stripes(self, area, angle, *args):
30 by gak
Added zero line to bar charts
488
        assert(area in Chart.VALID_SOLID_FILL_TYPES)
22 by gak
- pygooglechart.py converted to unix line breaks
489
        args = self._check_fill_linear(angle, *args)
490
        self.fill_types[area] = Chart.LINEAR_STRIPES
491
        self.fill_area[area] = ','.join([str(angle)] + args)
492
493
    def fill_to_url(self):
494
        areas = []
30 by gak
Added zero line to bar charts
495
        for area in (Chart.BACKGROUND, Chart.CHART, Chart.ALPHA):
22 by gak
- pygooglechart.py converted to unix line breaks
496
            if self.fill_types[area]:
497
                areas.append('%s,%s,%s' % (area, self.fill_types[area], \
498
                    self.fill_area[area]))
499
        if areas:
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
500
            return 'chf=' + '%7c'.join(areas)
22 by gak
- pygooglechart.py converted to unix line breaks
501
502
    # Data
503
    # -------------------------------------------------------------------------
504
505
    def data_class_detection(self, data):
506
        """Determines the appropriate data encoding type to give satisfactory
507
        resolution (http://code.google.com/apis/chart/#chart_data).
508
        """
509
        assert(isinstance(data, list) or isinstance(data, tuple))
510
        if not isinstance(self, (LineChart, BarChart, ScatterChart)):
511
            # From the link above:
512
            #   Simple encoding is suitable for all other types of chart
513
            #   regardless of size.
514
            return SimpleData
515
        elif self.height < 100:
516
            # The link above indicates that line and bar charts less
517
            # than 300px in size can be suitably represented with the
518
            # simple encoding. I've found that this isn't sufficient,
519
            # e.g. examples/line-xy-circle.png. Let's try 100px.
520
            return SimpleData
521
        else:
522
            return ExtendedData
523
39 by gak
- Bug fixed when automatic scaling is on and None values are in a data set (#5) (Alec Thomas)
524
    def _filter_none(self, data):
525
        return [r for r in data if r is not None]
526
22 by gak
- pygooglechart.py converted to unix line breaks
527
    def data_x_range(self):
528
        """Return a 2-tuple giving the minimum and maximum x-axis
529
        data range.
530
        """
531
        try:
39 by gak
- Bug fixed when automatic scaling is on and None values are in a data set (#5) (Alec Thomas)
532
            lower = min([min(self._filter_none(s))
533
                         for type, s in self.annotated_data()
22 by gak
- pygooglechart.py converted to unix line breaks
534
                         if type == 'x'])
39 by gak
- Bug fixed when automatic scaling is on and None values are in a data set (#5) (Alec Thomas)
535
            upper = max([max(self._filter_none(s))
536
                         for type, s in self.annotated_data()
22 by gak
- pygooglechart.py converted to unix line breaks
537
                         if type == 'x'])
538
            return (lower, upper)
539
        except ValueError:
540
            return None     # no x-axis datasets
541
542
    def data_y_range(self):
543
        """Return a 2-tuple giving the minimum and maximum y-axis
544
        data range.
545
        """
546
        try:
39 by gak
- Bug fixed when automatic scaling is on and None values are in a data set (#5) (Alec Thomas)
547
            lower = min([min(self._filter_none(s))
548
                         for type, s in self.annotated_data()
22 by gak
- pygooglechart.py converted to unix line breaks
549
                         if type == 'y'])
39 by gak
- Bug fixed when automatic scaling is on and None values are in a data set (#5) (Alec Thomas)
550
            upper = max([max(self._filter_none(s)) + 1
551
                         for type, s in self.annotated_data()
22 by gak
- pygooglechart.py converted to unix line breaks
552
                         if type == 'y'])
553
            return (lower, upper)
554
        except ValueError:
555
            return None     # no y-axis datasets
556
557
    def scaled_data(self, data_class, x_range=None, y_range=None):
558
        """Scale `self.data` as appropriate for the given data encoding
559
        (data_class) and return it.
560
561
        An optional `y_range` -- a 2-tuple (lower, upper) -- can be
562
        given to specify the y-axis bounds. If not given, the range is
563
        inferred from the data: (0, <max-value>) presuming no negative
564
        values, or (<min-value>, <max-value>) if there are negative
565
        values.  `self.scaled_y_range` is set to the actual lower and
566
        upper scaling range.
567
568
        Ditto for `x_range`. Note that some chart types don't have x-axis
569
        data.
570
        """
571
        self.scaled_data_class = data_class
572
573
        # Determine the x-axis range for scaling.
574
        if x_range is None:
575
            x_range = self.data_x_range()
576
            if x_range and x_range[0] > 0:
40 by gak
- Fixed a bug with auto-scaling, where the minimum was always 0. (#6) (Rohit Jenveja)
577
                x_range = (x_range[0], x_range[1])
22 by gak
- pygooglechart.py converted to unix line breaks
578
        self.scaled_x_range = x_range
579
580
        # Determine the y-axis range for scaling.
581
        if y_range is None:
582
            y_range = self.data_y_range()
583
            if y_range and y_range[0] > 0:
40 by gak
- Fixed a bug with auto-scaling, where the minimum was always 0. (#6) (Rohit Jenveja)
584
                y_range = (y_range[0], y_range[1])
22 by gak
- pygooglechart.py converted to unix line breaks
585
        self.scaled_y_range = y_range
586
587
        scaled_data = []
588
        for type, dataset in self.annotated_data():
589
            if type == 'x':
590
                scale_range = x_range
591
            elif type == 'y':
592
                scale_range = y_range
593
            elif type == 'marker-size':
594
                scale_range = (0, max(dataset))
39 by gak
- Bug fixed when automatic scaling is on and None values are in a data set (#5) (Alec Thomas)
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)
22 by gak
- pygooglechart.py converted to unix line breaks
603
        return scaled_data
604
605
    def add_data(self, data):
606
        self.data.append(data)
607
        return len(self.data) - 1  # return the "index" of the data set
608
609
    def data_to_url(self, data_class=None):
610
        if not data_class:
611
            data_class = self.data_class_detection(self.data)
612
        if not issubclass(data_class, Data):
613
            raise UnknownDataTypeException()
614
        if self.auto_scale:
615
            data = self.scaled_data(data_class, self.x_range, self.y_range)
616
        else:
617
            data = self.data
618
        return repr(data_class(data))
619
29 by gak
Added Google-o-meter chart
620
    def annotated_data(self):
621
        for dataset in self.data:
622
            yield ('x', dataset)
623
22 by gak
- pygooglechart.py converted to unix line breaks
624
    # Axis Labels
625
    # -------------------------------------------------------------------------
626
627
    def set_axis_labels(self, axis_type, values):
628
        assert(axis_type in Axis.TYPES)
68 by Gustav Hartvigsson
Made it compatible with Python 3.1
629
        values = [urllib.parse.quote(str(a)) for a in values]
22 by gak
- pygooglechart.py converted to unix line breaks
630
        axis_index = len(self.axis)
631
        axis = LabelAxis(axis_index, axis_type, values)
632
        self.axis.append(axis)
633
        return axis_index
634
635
    def set_axis_range(self, axis_type, low, high):
636
        assert(axis_type in Axis.TYPES)
637
        axis_index = len(self.axis)
638
        axis = RangeAxis(axis_index, axis_type, low, high)
639
        self.axis.append(axis)
640
        return axis_index
641
642
    def set_axis_positions(self, axis_index, positions):
643
        try:
644
            self.axis[axis_index].set_positions(positions)
645
        except IndexError:
646
            raise InvalidParametersException('Axis index %i has not been ' \
647
                'created' % axis)
648
649
    def set_axis_style(self, axis_index, colour, font_size=None, \
650
            alignment=None):
651
        try:
652
            self.axis[axis_index].set_style(colour, font_size, alignment)
653
        except IndexError:
654
            raise InvalidParametersException('Axis index %i has not been ' \
655
                'created' % axis)
656
657
    def axis_to_url(self):
658
        available_axis = []
659
        label_axis = []
660
        range_axis = []
661
        positions = []
662
        styles = []
663
        index = -1
664
        for axis in self.axis:
665
            available_axis.append(axis.axis_type)
666
            if isinstance(axis, RangeAxis):
667
                range_axis.append(repr(axis))
668
            if isinstance(axis, LabelAxis):
669
                label_axis.append(repr(axis))
670
            if axis.positions:
671
                positions.append(axis.positions_to_url())
672
            if axis.has_style:
673
                styles.append(axis.style_to_url())
674
        if not available_axis:
675
            return
676
        url_bits = []
677
        url_bits.append('chxt=%s' % ','.join(available_axis))
678
        if label_axis:
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
679
            url_bits.append('chxl=%s' % '%7c'.join(label_axis))
22 by gak
- pygooglechart.py converted to unix line breaks
680
        if range_axis:
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
681
            url_bits.append('chxr=%s' % '%7c'.join(range_axis))
22 by gak
- pygooglechart.py converted to unix line breaks
682
        if positions:
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
683
            url_bits.append('chxp=%s' % '%7c'.join(positions))
22 by gak
- pygooglechart.py converted to unix line breaks
684
        if styles:
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
685
            url_bits.append('chxs=%s' % '%7c'.join(styles))
22 by gak
- pygooglechart.py converted to unix line breaks
686
        return '&'.join(url_bits)
687
688
    # Markers, Ranges and Fill area (chm)
689
    # -------------------------------------------------------------------------
690
37 by gak
- Added "colours within series" option to chart. (chco=xxx|xxx) (Steve Brandt)
691
    def markers_to_url(self):        
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
692
        return 'chm=%s' % '%7c'.join([','.join(a) for a in self.markers])
22 by gak
- pygooglechart.py converted to unix line breaks
693
31 by gak
Added priority to shape markers
694
    def add_marker(self, index, point, marker_type, colour, size, priority=0):
22 by gak
- pygooglechart.py converted to unix line breaks
695
        self.markers.append((marker_type, colour, str(index), str(point), \
31 by gak
Added priority to shape markers
696
            str(size), str(priority)))
22 by gak
- pygooglechart.py converted to unix line breaks
697
698
    def add_horizontal_range(self, colour, start, stop):
37 by gak
- Added "colours within series" option to chart. (chco=xxx|xxx) (Steve Brandt)
699
        self.markers.append(('r', colour, '0', str(start), str(stop)))
700
701
    def add_data_line(self, colour, data_set, size, priority=0):
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
702
        self.markers.append(('D', colour, str(data_set), '0', str(size), \
703
            str(priority)))
37 by gak
- Added "colours within series" option to chart. (chco=xxx|xxx) (Steve Brandt)
704
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
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)))        
22 by gak
- pygooglechart.py converted to unix line breaks
709
710
    def add_vertical_range(self, colour, start, stop):
37 by gak
- Added "colours within series" option to chart. (chco=xxx|xxx) (Steve Brandt)
711
        self.markers.append(('R', colour, '0', str(start), str(stop)))
22 by gak
- pygooglechart.py converted to unix line breaks
712
713
    def add_fill_range(self, colour, index_start, index_end):
714
        self.markers.append(('b', colour, str(index_start), str(index_end), \
715
            '1'))
716
717
    def add_fill_simple(self, colour):
718
        self.markers.append(('B', colour, '1', '1', '1'))
719
27 by gak
grids and line styles are not only restricted to line chart types
720
    # Line styles
721
    # -------------------------------------------------------------------------
22 by gak
- pygooglechart.py converted to unix line breaks
722
723
    def set_line_style(self, index, thickness=1, line_segment=None, \
724
            blank_segment=None):
725
        value = []
726
        value.append(str(thickness))
727
        if line_segment:
728
            value.append(str(line_segment))
729
            value.append(str(blank_segment))
730
        self.line_styles[index] = value
731
27 by gak
grids and line styles are not only restricted to line chart types
732
    # Grid
733
    # -------------------------------------------------------------------------
734
22 by gak
- pygooglechart.py converted to unix line breaks
735
    def set_grid(self, x_step, y_step, line_segment=1, \
736
            blank_segment=0):
737
        self.grid = '%s,%s,%s,%s' % (x_step, y_step, line_segment, \
738
            blank_segment)
739
27 by gak
grids and line styles are not only restricted to line chart types
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
33 by gak
pep8 fixes, version bump
754
27 by gak
grids and line styles are not only restricted to line chart types
755
class LineChart(Chart):
756
757
    def __init__(self, *args, **kwargs):
35 by gak
- Initial "grammar" code
758
        if type(self) == LineChart:
759
            raise AbstractClassException('This is an abstract class')
27 by gak
grids and line styles are not only restricted to line chart types
760
        Chart.__init__(self, *args, **kwargs)
761
22 by gak
- pygooglechart.py converted to unix line breaks
762
763
class SimpleLineChart(LineChart):
764
765
    def type_to_url(self):
766
        return 'cht=lc'
767
768
    def annotated_data(self):
769
        # All datasets are y-axis data.
770
        for dataset in self.data:
771
            yield ('y', dataset)
772
33 by gak
pep8 fixes, version bump
773
22 by gak
- pygooglechart.py converted to unix line breaks
774
class SparkLineChart(SimpleLineChart):
775
776
    def type_to_url(self):
777
        return 'cht=ls'
778
33 by gak
pep8 fixes, version bump
779
22 by gak
- pygooglechart.py converted to unix line breaks
780
class XYLineChart(LineChart):
781
782
    def type_to_url(self):
783
        return 'cht=lxy'
784
785
    def annotated_data(self):
786
        # Datasets alternate between x-axis, y-axis.
787
        for i, dataset in enumerate(self.data):
788
            if i % 2 == 0:
789
                yield ('x', dataset)
790
            else:
791
                yield ('y', dataset)
792
33 by gak
pep8 fixes, version bump
793
22 by gak
- pygooglechart.py converted to unix line breaks
794
class BarChart(Chart):
795
796
    def __init__(self, *args, **kwargs):
35 by gak
- Initial "grammar" code
797
        if type(self) == BarChart:
798
            raise AbstractClassException('This is an abstract class')
22 by gak
- pygooglechart.py converted to unix line breaks
799
        Chart.__init__(self, *args, **kwargs)
800
        self.bar_width = None
30 by gak
Added zero line to bar charts
801
        self.zero_lines = {}
22 by gak
- pygooglechart.py converted to unix line breaks
802
803
    def set_bar_width(self, bar_width):
804
        self.bar_width = bar_width
805
30 by gak
Added zero line to bar charts
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):
25 by gak
Autoscale fixes and refactoring by Grahm Ullrich
810
        url_bits = Chart.get_url_bits(self, data_class=data_class)
30 by gak
Added zero line to bar charts
811
        if not skip_chbh and self.bar_width is not None:
22 by gak
- pygooglechart.py converted to unix line breaks
812
            url_bits.append('chbh=%i' % self.bar_width)
30 by gak
Added zero line to bar charts
813
        zero_line = []
814
        if self.zero_lines:
68 by Gustav Hartvigsson
Made it compatible with Python 3.1
815
            for index in range(max(self.zero_lines) + 1):
30 by gak
Added zero line to bar charts
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))
22 by gak
- pygooglechart.py converted to unix line breaks
821
        return url_bits
822
823
824
class StackedHorizontalBarChart(BarChart):
825
826
    def type_to_url(self):
827
        return 'cht=bhs'
828
829
830
class StackedVerticalBarChart(BarChart):
831
832
    def type_to_url(self):
833
        return 'cht=bvs'
834
835
    def annotated_data(self):
836
        for dataset in self.data:
837
            yield ('y', dataset)
838
839
840
class GroupedBarChart(BarChart):
841
842
    def __init__(self, *args, **kwargs):
35 by gak
- Initial "grammar" code
843
        if type(self) == GroupedBarChart:
844
            raise AbstractClassException('This is an abstract class')
22 by gak
- pygooglechart.py converted to unix line breaks
845
        BarChart.__init__(self, *args, **kwargs)
846
        self.bar_spacing = None
847
        self.group_spacing = None
848
849
    def set_bar_spacing(self, spacing):
850
        """Set spacing between bars in a group."""
851
        self.bar_spacing = spacing
852
853
    def set_group_spacing(self, spacing):
854
        """Set spacing between groups of bars."""
855
        self.group_spacing = spacing
856
25 by gak
Autoscale fixes and refactoring by Grahm Ullrich
857
    def get_url_bits(self, data_class=None):
22 by gak
- pygooglechart.py converted to unix line breaks
858
        # Skip 'BarChart.get_url_bits' and call Chart directly so the parent
859
        # doesn't add "chbh" before we do.
30 by gak
Added zero line to bar charts
860
        url_bits = BarChart.get_url_bits(self, data_class=data_class,
861
            skip_chbh=True)
22 by gak
- pygooglechart.py converted to unix line breaks
862
        if self.group_spacing is not None:
863
            if self.bar_spacing is None:
33 by gak
pep8 fixes, version bump
864
                raise InvalidParametersException('Bar spacing is required ' \
865
                    'to be set when setting group spacing')
22 by gak
- pygooglechart.py converted to unix line breaks
866
            if self.bar_width is None:
867
                raise InvalidParametersException('Bar width is required to ' \
868
                    'be set when setting bar spacing')
869
            url_bits.append('chbh=%i,%i,%i'
870
                % (self.bar_width, self.bar_spacing, self.group_spacing))
871
        elif self.bar_spacing is not None:
872
            if self.bar_width is None:
873
                raise InvalidParametersException('Bar width is required to ' \
874
                    'be set when setting bar spacing')
875
            url_bits.append('chbh=%i,%i' % (self.bar_width, self.bar_spacing))
27 by gak
grids and line styles are not only restricted to line chart types
876
        elif self.bar_width:
22 by gak
- pygooglechart.py converted to unix line breaks
877
            url_bits.append('chbh=%i' % self.bar_width)
878
        return url_bits
879
880
881
class GroupedHorizontalBarChart(GroupedBarChart):
882
883
    def type_to_url(self):
884
        return 'cht=bhg'
885
886
887
class GroupedVerticalBarChart(GroupedBarChart):
888
889
    def type_to_url(self):
890
        return 'cht=bvg'
891
892
    def annotated_data(self):
893
        for dataset in self.data:
894
            yield ('y', dataset)
895
896
897
class PieChart(Chart):
898
899
    def __init__(self, *args, **kwargs):
35 by gak
- Initial "grammar" code
900
        if type(self) == PieChart:
901
            raise AbstractClassException('This is an abstract class')
22 by gak
- pygooglechart.py converted to unix line breaks
902
        Chart.__init__(self, *args, **kwargs)
903
        self.pie_labels = []
36 by gak
- Really added initial unit tests
904
        if self.y_range:
905
            warnings.warn('y_range is not used with %s.' % \
906
                (self.__class__.__name__))
22 by gak
- pygooglechart.py converted to unix line breaks
907
908
    def set_pie_labels(self, labels):
68 by Gustav Hartvigsson
Made it compatible with Python 3.1
909
        self.pie_labels = [urllib.parse.quote(a) for a in labels]
22 by gak
- pygooglechart.py converted to unix line breaks
910
25 by gak
Autoscale fixes and refactoring by Grahm Ullrich
911
    def get_url_bits(self, data_class=None):
912
        url_bits = Chart.get_url_bits(self, data_class=data_class)
22 by gak
- pygooglechart.py converted to unix line breaks
913
        if self.pie_labels:
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
914
            url_bits.append('chl=%s' % '%7c'.join(self.pie_labels))
22 by gak
- pygooglechart.py converted to unix line breaks
915
        return url_bits
916
917
    def annotated_data(self):
918
        # Datasets are all y-axis data. However, there should only be
919
        # one dataset for pie charts.
920
        for dataset in self.data:
36 by gak
- Really added initial unit tests
921
            yield ('x', dataset)
22 by gak
- pygooglechart.py converted to unix line breaks
922
47 by gak
- Fixed bug with automatic scaling and pie charts
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)
927
22 by gak
- pygooglechart.py converted to unix line breaks
928
929
class PieChart2D(PieChart):
930
931
    def type_to_url(self):
932
        return 'cht=p'
933
934
935
class PieChart3D(PieChart):
936
937
    def type_to_url(self):
938
        return 'cht=p3'
939
940
941
class VennChart(Chart):
942
943
    def type_to_url(self):
944
        return 'cht=v'
945
946
    def annotated_data(self):
947
        for dataset in self.data:
948
            yield ('y', dataset)
949
950
26 by gak
Initial radar chart implementation
951
class RadarChart(Chart):
952
953
    def type_to_url(self):
954
        return 'cht=r'
955
33 by gak
pep8 fixes, version bump
956
28 by gak
Added map chart type
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 = []
58 by gak
- Version bump to 0.3.0
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')
57 by gak
- #23 MapChart extensions (thanks to Andreas Schawo)
995
        
28 by gak
Added map chart type
996
    def type_to_url(self):
997
        return 'cht=t'
998
999
    def set_codes(self, codes):
57 by gak
- #23 MapChart extensions (thanks to Andreas Schawo)
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)
28 by gak
Added map chart type
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
57 by gak
- #23 MapChart extensions (thanks to Andreas Schawo)
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
29 by gak
Added Google-o-meter chart
1052
1053
class GoogleOMeterChart(PieChart):
1054
    """Inheriting from PieChart because of similar labeling"""
1055
36 by gak
- Really added initial unit tests
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
29 by gak
Added Google-o-meter chart
1062
    def type_to_url(self):
1063
        return 'cht=gom'
1064
26 by gak
Initial radar chart implementation
1065
38 by gak
- Added support for QR Code chart (#8)
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()
68 by Gustav Hartvigsson
Made it compatible with Python 3.1
1080
        return 'chl=%s' % urllib.parse.quote(self.data[0])
38 by gak
- Added support for QR Code chart (#8)
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:
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
1087
            url_bits.append('chld=%s%%7c%s' % (self.ec_level, self.margin))
38 by gak
- Added support for QR Code chart (#8)
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
35 by gak
- Initial "grammar" code
1098
class ChartGrammar(object):
1099
36 by gak
- Really added initial unit tests
1100
    def __init__(self):
1101
        self.grammar = None
1102
        self.chart = None
1103
1104
    def parse(self, grammar):
35 by gak
- Initial "grammar" code
1105
        self.grammar = grammar
1106
        self.chart = self.create_chart_instance()
1107
36 by gak
- Really added initial unit tests
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
35 by gak
- Initial "grammar" code
1122
    @staticmethod
1123
    def get_possible_chart_types():
1124
        possible_charts = []
36 by gak
- Really added initial unit tests
1125
        for cls_name in globals().keys():
35 by gak
- Initial "grammar" code
1126
            if not cls_name.endswith('Chart'):
1127
                continue
1128
            cls = globals()[cls_name]
1129
            # Check if it is an abstract class
1130
            try:
36 by gak
- Really added initial unit tests
1131
                a = cls(1, 1, auto_scale=False)
1132
                del a
35 by gak
- Initial "grammar" code
1133
            except AbstractClassException:
1134
                continue
1135
            # Strip off "Class"
1136
            possible_charts.append(cls_name[:-5])
1137
        return possible_charts
1138
36 by gak
- Really added initial unit tests
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
35 by gak
- Initial "grammar" code
1143
        assert('w' in grammar)  # width is required
1144
        assert('h' in grammar)  # height is required
1145
        assert('type' in grammar)  # type is required
36 by gak
- Really added initial unit tests
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)
35 by gak
- Initial "grammar" code
1152
        types = ChartGrammar.get_possible_chart_types()
36 by gak
- Really added initial unit tests
1153
        if chart_type not in types:
35 by gak
- Initial "grammar" code
1154
            raise UnknownChartType('%s is an unknown chart type. Possible '
36 by gak
- Really added initial unit tests
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)
35 by gak
- Initial "grammar" code
1158
1159
    def download(self):
1160
        pass
1161