/+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
26
import urllib2
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
62 by Stefan Hacker
POST requests. Closes #1
339
        
340
        return self.BASE_URL + '?' + self.get_url_extension(data_class)
341
    
342
    def get_url_extension(self, data_class = None):
22 by gak
- pygooglechart.py converted to unix line breaks
343
    # -------------------------------------------------------------------------
344
25 by gak
Autoscale fixes and refactoring by Grahm Ullrich
345
    def get_url(self, data_class=None):
346
        url_bits = self.get_url_bits(data_class=data_class)
62 by Stefan Hacker
POST requests. Closes #1
347
        return '&'.join(url_bits)
22 by gak
- pygooglechart.py converted to unix line breaks
348
25 by gak
Autoscale fixes and refactoring by Grahm Ullrich
349
    def get_url_bits(self, data_class=None):
22 by gak
- pygooglechart.py converted to unix line breaks
350
        url_bits = []
351
        # required arguments
352
        url_bits.append(self.type_to_url())
353
        url_bits.append('chs=%ix%i' % (self.width, self.height))
25 by gak
Autoscale fixes and refactoring by Grahm Ullrich
354
        url_bits.append(self.data_to_url(data_class=data_class))
22 by gak
- pygooglechart.py converted to unix line breaks
355
        # optional arguments
356
        if self.title:
357
            url_bits.append('chtt=%s' % self.title)
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
358
        if self.title_colour and self.title_font_size:
359
            url_bits.append('chts=%s,%s' % (self.title_colour, \
360
                self.title_font_size))
22 by gak
- pygooglechart.py converted to unix line breaks
361
        if self.legend:
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
362
            url_bits.append('chdl=%s' % '%7c'.join(self.legend))
37 by gak
- Added "colours within series" option to chart. (chco=xxx|xxx) (Steve Brandt)
363
        if self.legend_position:
364
            url_bits.append('chdlp=%s' % (self.legend_position))
22 by gak
- pygooglechart.py converted to unix line breaks
365
        if self.colours:
37 by gak
- Added "colours within series" option to chart. (chco=xxx|xxx) (Steve Brandt)
366
            url_bits.append('chco=%s' % ','.join(self.colours))            
367
        if self.colours_within_series:
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
368
            url_bits.append('chco=%s' % '%7c'.join(self.colours_within_series))
22 by gak
- pygooglechart.py converted to unix line breaks
369
        ret = self.fill_to_url()
370
        if ret:
371
            url_bits.append(ret)
372
        ret = self.axis_to_url()
373
        if ret:
37 by gak
- Added "colours within series" option to chart. (chco=xxx|xxx) (Steve Brandt)
374
            url_bits.append(ret)                    
22 by gak
- pygooglechart.py converted to unix line breaks
375
        if self.markers:
37 by gak
- Added "colours within series" option to chart. (chco=xxx|xxx) (Steve Brandt)
376
            url_bits.append(self.markers_to_url())        
27 by gak
grids and line styles are not only restricted to line chart types
377
        if self.line_styles:
378
            style = []
379
            for index in xrange(max(self.line_styles) + 1):
380
                if index in self.line_styles:
381
                    values = self.line_styles[index]
382
                else:
383
                    values = ('1', )
384
                style.append(','.join(values))
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
385
            url_bits.append('chls=%s' % '%7c'.join(style))
27 by gak
grids and line styles are not only restricted to line chart types
386
        if self.grid:
387
            url_bits.append('chg=%s' % self.grid)
22 by gak
- pygooglechart.py converted to unix line breaks
388
        return url_bits
389
390
    # Downloading
391
    # -------------------------------------------------------------------------
392
62 by Stefan Hacker
POST requests. Closes #1
393
    def download(self, file_name, use_post = False):
394
        if use_post:
395
            opener = urllib2.urlopen(self.BASE_URL, self.get_url_extension())
396
        else:
397
            opener = urllib2.urlopen(self.get_url())
22 by gak
- pygooglechart.py converted to unix line breaks
398
399
        if opener.headers['content-type'] != 'image/png':
400
            raise BadContentTypeException('Server responded with a ' \
401
                'content-type of %s' % opener.headers['content-type'])
402
41 by gak
Fixed bug where the module would download twice (#7) (Evan Lezar)
403
        open(file_name, 'wb').write(opener.read())
22 by gak
- pygooglechart.py converted to unix line breaks
404
405
    # Simple settings
406
    # -------------------------------------------------------------------------
407
408
    def set_title(self, title):
409
        if title:
410
            self.title = urllib.quote(title)
411
        else:
412
            self.title = None
413
60 by gak
- #16 Allow either arguments in set_title_style()
414
    def set_title_style(self, colour=None, font_size=None):
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
415
        if not colour is None:
416
            _check_colour(colour)
60 by gak
- #16 Allow either arguments in set_title_style()
417
        if not colour and not font_size:
418
            return
419
        self.title_colour = colour or '333333'
420
        self.title_font_size = font_size or 13.5
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
421
22 by gak
- pygooglechart.py converted to unix line breaks
422
    def set_legend(self, legend):
423
        """legend needs to be a list, tuple or None"""
424
        assert(isinstance(legend, list) or isinstance(legend, tuple) or
425
            legend is None)
426
        if legend:
427
            self.legend = [urllib.quote(a) for a in legend]
428
        else:
429
            self.legend = None
430
37 by gak
- Added "colours within series" option to chart. (chco=xxx|xxx) (Steve Brandt)
431
    def set_legend_position(self, legend_position):
432
        if legend_position:
433
            self.legend_position = urllib.quote(legend_position)
434
        else:    
435
            self.legend_position = None
436
22 by gak
- pygooglechart.py converted to unix line breaks
437
    # Chart colours
438
    # -------------------------------------------------------------------------
439
440
    def set_colours(self, colours):
441
        # colours needs to be a list, tuple or None
442
        assert(isinstance(colours, list) or isinstance(colours, tuple) or
443
            colours is None)
444
        # make sure the colours are in the right format
445
        if colours:
446
            for col in colours:
447
                _check_colour(col)
448
        self.colours = colours
449
37 by gak
- Added "colours within series" option to chart. (chco=xxx|xxx) (Steve Brandt)
450
    def set_colours_within_series(self, colours):
451
        # colours needs to be a list, tuple or None
452
        assert(isinstance(colours, list) or isinstance(colours, tuple) or
453
            colours is None)
454
        # make sure the colours are in the right format
455
        if colours:
456
            for col in colours:
457
                _check_colour(col)
458
        self.colours_within_series = colours        
459
22 by gak
- pygooglechart.py converted to unix line breaks
460
    # Background/Chart colours
461
    # -------------------------------------------------------------------------
462
463
    def fill_solid(self, area, colour):
30 by gak
Added zero line to bar charts
464
        assert(area in Chart.VALID_SOLID_FILL_TYPES)
22 by gak
- pygooglechart.py converted to unix line breaks
465
        _check_colour(colour)
466
        self.fill_area[area] = colour
467
        self.fill_types[area] = Chart.SOLID
468
469
    def _check_fill_linear(self, angle, *args):
470
        assert(isinstance(args, list) or isinstance(args, tuple))
471
        assert(angle >= 0 and angle <= 90)
472
        assert(len(args) % 2 == 0)
473
        args = list(args)  # args is probably a tuple and we need to mutate
53 by gak
Fixed a deprecation warning
474
        for a in xrange(int(len(args) / 2)):
22 by gak
- pygooglechart.py converted to unix line breaks
475
            col = args[a * 2]
476
            offset = args[a * 2 + 1]
477
            _check_colour(col)
478
            assert(offset >= 0 and offset <= 1)
479
            args[a * 2 + 1] = str(args[a * 2 + 1])
480
        return args
481
482
    def fill_linear_gradient(self, area, angle, *args):
30 by gak
Added zero line to bar charts
483
        assert(area in Chart.VALID_SOLID_FILL_TYPES)
22 by gak
- pygooglechart.py converted to unix line breaks
484
        args = self._check_fill_linear(angle, *args)
485
        self.fill_types[area] = Chart.LINEAR_GRADIENT
486
        self.fill_area[area] = ','.join([str(angle)] + args)
487
488
    def fill_linear_stripes(self, area, angle, *args):
30 by gak
Added zero line to bar charts
489
        assert(area in Chart.VALID_SOLID_FILL_TYPES)
22 by gak
- pygooglechart.py converted to unix line breaks
490
        args = self._check_fill_linear(angle, *args)
491
        self.fill_types[area] = Chart.LINEAR_STRIPES
492
        self.fill_area[area] = ','.join([str(angle)] + args)
493
494
    def fill_to_url(self):
495
        areas = []
30 by gak
Added zero line to bar charts
496
        for area in (Chart.BACKGROUND, Chart.CHART, Chart.ALPHA):
22 by gak
- pygooglechart.py converted to unix line breaks
497
            if self.fill_types[area]:
498
                areas.append('%s,%s,%s' % (area, self.fill_types[area], \
499
                    self.fill_area[area]))
500
        if areas:
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
501
            return 'chf=' + '%7c'.join(areas)
22 by gak
- pygooglechart.py converted to unix line breaks
502
503
    # Data
504
    # -------------------------------------------------------------------------
505
506
    def data_class_detection(self, data):
507
        """Determines the appropriate data encoding type to give satisfactory
508
        resolution (http://code.google.com/apis/chart/#chart_data).
509
        """
510
        assert(isinstance(data, list) or isinstance(data, tuple))
511
        if not isinstance(self, (LineChart, BarChart, ScatterChart)):
512
            # From the link above:
513
            #   Simple encoding is suitable for all other types of chart
514
            #   regardless of size.
515
            return SimpleData
516
        elif self.height < 100:
517
            # The link above indicates that line and bar charts less
518
            # than 300px in size can be suitably represented with the
519
            # simple encoding. I've found that this isn't sufficient,
520
            # e.g. examples/line-xy-circle.png. Let's try 100px.
521
            return SimpleData
522
        else:
523
            return ExtendedData
524
39 by gak
- Bug fixed when automatic scaling is on and None values are in a data set (#5) (Alec Thomas)
525
    def _filter_none(self, data):
526
        return [r for r in data if r is not None]
527
22 by gak
- pygooglechart.py converted to unix line breaks
528
    def data_x_range(self):
529
        """Return a 2-tuple giving the minimum and maximum x-axis
530
        data range.
531
        """
532
        try:
39 by gak
- Bug fixed when automatic scaling is on and None values are in a data set (#5) (Alec Thomas)
533
            lower = min([min(self._filter_none(s))
534
                         for type, s in self.annotated_data()
22 by gak
- pygooglechart.py converted to unix line breaks
535
                         if type == 'x'])
39 by gak
- Bug fixed when automatic scaling is on and None values are in a data set (#5) (Alec Thomas)
536
            upper = max([max(self._filter_none(s))
537
                         for type, s in self.annotated_data()
22 by gak
- pygooglechart.py converted to unix line breaks
538
                         if type == 'x'])
539
            return (lower, upper)
540
        except ValueError:
541
            return None     # no x-axis datasets
542
543
    def data_y_range(self):
544
        """Return a 2-tuple giving the minimum and maximum y-axis
545
        data range.
546
        """
547
        try:
39 by gak
- Bug fixed when automatic scaling is on and None values are in a data set (#5) (Alec Thomas)
548
            lower = min([min(self._filter_none(s))
549
                         for type, s in self.annotated_data()
22 by gak
- pygooglechart.py converted to unix line breaks
550
                         if type == 'y'])
39 by gak
- Bug fixed when automatic scaling is on and None values are in a data set (#5) (Alec Thomas)
551
            upper = max([max(self._filter_none(s)) + 1
552
                         for type, s in self.annotated_data()
22 by gak
- pygooglechart.py converted to unix line breaks
553
                         if type == 'y'])
554
            return (lower, upper)
555
        except ValueError:
556
            return None     # no y-axis datasets
557
558
    def scaled_data(self, data_class, x_range=None, y_range=None):
559
        """Scale `self.data` as appropriate for the given data encoding
560
        (data_class) and return it.
561
562
        An optional `y_range` -- a 2-tuple (lower, upper) -- can be
563
        given to specify the y-axis bounds. If not given, the range is
564
        inferred from the data: (0, <max-value>) presuming no negative
565
        values, or (<min-value>, <max-value>) if there are negative
566
        values.  `self.scaled_y_range` is set to the actual lower and
567
        upper scaling range.
568
569
        Ditto for `x_range`. Note that some chart types don't have x-axis
570
        data.
571
        """
572
        self.scaled_data_class = data_class
573
574
        # Determine the x-axis range for scaling.
575
        if x_range is None:
576
            x_range = self.data_x_range()
577
            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)
578
                x_range = (x_range[0], x_range[1])
22 by gak
- pygooglechart.py converted to unix line breaks
579
        self.scaled_x_range = x_range
580
581
        # Determine the y-axis range for scaling.
582
        if y_range is None:
583
            y_range = self.data_y_range()
584
            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)
585
                y_range = (y_range[0], y_range[1])
22 by gak
- pygooglechart.py converted to unix line breaks
586
        self.scaled_y_range = y_range
587
588
        scaled_data = []
589
        for type, dataset in self.annotated_data():
590
            if type == 'x':
591
                scale_range = x_range
592
            elif type == 'y':
593
                scale_range = y_range
594
            elif type == 'marker-size':
595
                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)
596
            scaled_dataset = []
597
            for v in dataset:
598
                if v is None:
599
                    scaled_dataset.append(None)
600
                else:
601
                    scaled_dataset.append(
602
                        data_class.scale_value(v, scale_range))
603
            scaled_data.append(scaled_dataset)
22 by gak
- pygooglechart.py converted to unix line breaks
604
        return scaled_data
605
606
    def add_data(self, data):
607
        self.data.append(data)
608
        return len(self.data) - 1  # return the "index" of the data set
609
610
    def data_to_url(self, data_class=None):
611
        if not data_class:
612
            data_class = self.data_class_detection(self.data)
613
        if not issubclass(data_class, Data):
614
            raise UnknownDataTypeException()
615
        if self.auto_scale:
616
            data = self.scaled_data(data_class, self.x_range, self.y_range)
617
        else:
618
            data = self.data
619
        return repr(data_class(data))
620
29 by gak
Added Google-o-meter chart
621
    def annotated_data(self):
622
        for dataset in self.data:
623
            yield ('x', dataset)
624
22 by gak
- pygooglechart.py converted to unix line breaks
625
    # Axis Labels
626
    # -------------------------------------------------------------------------
627
628
    def set_axis_labels(self, axis_type, values):
629
        assert(axis_type in Axis.TYPES)
47 by gak
- Fixed bug with automatic scaling and pie charts
630
        values = [urllib.quote(str(a)) for a in values]
22 by gak
- pygooglechart.py converted to unix line breaks
631
        axis_index = len(self.axis)
632
        axis = LabelAxis(axis_index, axis_type, values)
633
        self.axis.append(axis)
634
        return axis_index
635
636
    def set_axis_range(self, axis_type, low, high):
637
        assert(axis_type in Axis.TYPES)
638
        axis_index = len(self.axis)
639
        axis = RangeAxis(axis_index, axis_type, low, high)
640
        self.axis.append(axis)
641
        return axis_index
642
643
    def set_axis_positions(self, axis_index, positions):
644
        try:
645
            self.axis[axis_index].set_positions(positions)
646
        except IndexError:
647
            raise InvalidParametersException('Axis index %i has not been ' \
648
                'created' % axis)
649
650
    def set_axis_style(self, axis_index, colour, font_size=None, \
651
            alignment=None):
652
        try:
653
            self.axis[axis_index].set_style(colour, font_size, alignment)
654
        except IndexError:
655
            raise InvalidParametersException('Axis index %i has not been ' \
656
                'created' % axis)
657
658
    def axis_to_url(self):
659
        available_axis = []
660
        label_axis = []
661
        range_axis = []
662
        positions = []
663
        styles = []
664
        index = -1
665
        for axis in self.axis:
666
            available_axis.append(axis.axis_type)
667
            if isinstance(axis, RangeAxis):
668
                range_axis.append(repr(axis))
669
            if isinstance(axis, LabelAxis):
670
                label_axis.append(repr(axis))
671
            if axis.positions:
672
                positions.append(axis.positions_to_url())
673
            if axis.has_style:
674
                styles.append(axis.style_to_url())
675
        if not available_axis:
676
            return
677
        url_bits = []
678
        url_bits.append('chxt=%s' % ','.join(available_axis))
679
        if label_axis:
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
680
            url_bits.append('chxl=%s' % '%7c'.join(label_axis))
22 by gak
- pygooglechart.py converted to unix line breaks
681
        if range_axis:
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
682
            url_bits.append('chxr=%s' % '%7c'.join(range_axis))
22 by gak
- pygooglechart.py converted to unix line breaks
683
        if positions:
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
684
            url_bits.append('chxp=%s' % '%7c'.join(positions))
22 by gak
- pygooglechart.py converted to unix line breaks
685
        if styles:
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
686
            url_bits.append('chxs=%s' % '%7c'.join(styles))
22 by gak
- pygooglechart.py converted to unix line breaks
687
        return '&'.join(url_bits)
688
689
    # Markers, Ranges and Fill area (chm)
690
    # -------------------------------------------------------------------------
691
37 by gak
- Added "colours within series" option to chart. (chco=xxx|xxx) (Steve Brandt)
692
    def markers_to_url(self):        
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
693
        return 'chm=%s' % '%7c'.join([','.join(a) for a in self.markers])
22 by gak
- pygooglechart.py converted to unix line breaks
694
31 by gak
Added priority to shape markers
695
    def add_marker(self, index, point, marker_type, colour, size, priority=0):
22 by gak
- pygooglechart.py converted to unix line breaks
696
        self.markers.append((marker_type, colour, str(index), str(point), \
31 by gak
Added priority to shape markers
697
            str(size), str(priority)))
22 by gak
- pygooglechart.py converted to unix line breaks
698
699
    def add_horizontal_range(self, colour, start, stop):
37 by gak
- Added "colours within series" option to chart. (chco=xxx|xxx) (Steve Brandt)
700
        self.markers.append(('r', colour, '0', str(start), str(stop)))
701
702
    def add_data_line(self, colour, data_set, size, priority=0):
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
703
        self.markers.append(('D', colour, str(data_set), '0', str(size), \
704
            str(priority)))
37 by gak
- Added "colours within series" option to chart. (chco=xxx|xxx) (Steve Brandt)
705
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
706
    def add_marker_text(self, string, colour, data_set, data_point, size, \
707
            priority=0):
708
        self.markers.append((str(string), colour, str(data_set), \
709
            str(data_point), str(size), str(priority)))        
22 by gak
- pygooglechart.py converted to unix line breaks
710
711
    def add_vertical_range(self, colour, start, stop):
37 by gak
- Added "colours within series" option to chart. (chco=xxx|xxx) (Steve Brandt)
712
        self.markers.append(('R', colour, '0', str(start), str(stop)))
22 by gak
- pygooglechart.py converted to unix line breaks
713
714
    def add_fill_range(self, colour, index_start, index_end):
715
        self.markers.append(('b', colour, str(index_start), str(index_end), \
716
            '1'))
717
718
    def add_fill_simple(self, colour):
719
        self.markers.append(('B', colour, '1', '1', '1'))
720
27 by gak
grids and line styles are not only restricted to line chart types
721
    # Line styles
722
    # -------------------------------------------------------------------------
22 by gak
- pygooglechart.py converted to unix line breaks
723
724
    def set_line_style(self, index, thickness=1, line_segment=None, \
725
            blank_segment=None):
726
        value = []
727
        value.append(str(thickness))
728
        if line_segment:
729
            value.append(str(line_segment))
730
            value.append(str(blank_segment))
731
        self.line_styles[index] = value
732
27 by gak
grids and line styles are not only restricted to line chart types
733
    # Grid
734
    # -------------------------------------------------------------------------
735
22 by gak
- pygooglechart.py converted to unix line breaks
736
    def set_grid(self, x_step, y_step, line_segment=1, \
737
            blank_segment=0):
738
        self.grid = '%s,%s,%s,%s' % (x_step, y_step, line_segment, \
739
            blank_segment)
740
27 by gak
grids and line styles are not only restricted to line chart types
741
742
class ScatterChart(Chart):
743
744
    def type_to_url(self):
745
        return 'cht=s'
746
747
    def annotated_data(self):
748
        yield ('x', self.data[0])
749
        yield ('y', self.data[1])
750
        if len(self.data) > 2:
751
            # The optional third dataset is relative sizing for point
752
            # markers.
753
            yield ('marker-size', self.data[2])
754
33 by gak
pep8 fixes, version bump
755
27 by gak
grids and line styles are not only restricted to line chart types
756
class LineChart(Chart):
757
758
    def __init__(self, *args, **kwargs):
35 by gak
- Initial "grammar" code
759
        if type(self) == LineChart:
760
            raise AbstractClassException('This is an abstract class')
27 by gak
grids and line styles are not only restricted to line chart types
761
        Chart.__init__(self, *args, **kwargs)
762
22 by gak
- pygooglechart.py converted to unix line breaks
763
764
class SimpleLineChart(LineChart):
765
766
    def type_to_url(self):
767
        return 'cht=lc'
768
769
    def annotated_data(self):
770
        # All datasets are y-axis data.
771
        for dataset in self.data:
772
            yield ('y', dataset)
773
33 by gak
pep8 fixes, version bump
774
22 by gak
- pygooglechart.py converted to unix line breaks
775
class SparkLineChart(SimpleLineChart):
776
777
    def type_to_url(self):
778
        return 'cht=ls'
779
33 by gak
pep8 fixes, version bump
780
22 by gak
- pygooglechart.py converted to unix line breaks
781
class XYLineChart(LineChart):
782
783
    def type_to_url(self):
784
        return 'cht=lxy'
785
786
    def annotated_data(self):
787
        # Datasets alternate between x-axis, y-axis.
788
        for i, dataset in enumerate(self.data):
789
            if i % 2 == 0:
790
                yield ('x', dataset)
791
            else:
792
                yield ('y', dataset)
793
33 by gak
pep8 fixes, version bump
794
22 by gak
- pygooglechart.py converted to unix line breaks
795
class BarChart(Chart):
796
797
    def __init__(self, *args, **kwargs):
35 by gak
- Initial "grammar" code
798
        if type(self) == BarChart:
799
            raise AbstractClassException('This is an abstract class')
22 by gak
- pygooglechart.py converted to unix line breaks
800
        Chart.__init__(self, *args, **kwargs)
801
        self.bar_width = None
30 by gak
Added zero line to bar charts
802
        self.zero_lines = {}
22 by gak
- pygooglechart.py converted to unix line breaks
803
804
    def set_bar_width(self, bar_width):
805
        self.bar_width = bar_width
806
30 by gak
Added zero line to bar charts
807
    def set_zero_line(self, index, zero_line):
808
        self.zero_lines[index] = zero_line
809
810
    def get_url_bits(self, data_class=None, skip_chbh=False):
25 by gak
Autoscale fixes and refactoring by Grahm Ullrich
811
        url_bits = Chart.get_url_bits(self, data_class=data_class)
30 by gak
Added zero line to bar charts
812
        if not skip_chbh and self.bar_width is not None:
22 by gak
- pygooglechart.py converted to unix line breaks
813
            url_bits.append('chbh=%i' % self.bar_width)
30 by gak
Added zero line to bar charts
814
        zero_line = []
815
        if self.zero_lines:
816
            for index in xrange(max(self.zero_lines) + 1):
817
                if index in self.zero_lines:
818
                    zero_line.append(str(self.zero_lines[index]))
819
                else:
820
                    zero_line.append('0')
821
            url_bits.append('chp=%s' % ','.join(zero_line))
22 by gak
- pygooglechart.py converted to unix line breaks
822
        return url_bits
823
824
825
class StackedHorizontalBarChart(BarChart):
826
827
    def type_to_url(self):
828
        return 'cht=bhs'
829
830
831
class StackedVerticalBarChart(BarChart):
832
833
    def type_to_url(self):
834
        return 'cht=bvs'
835
836
    def annotated_data(self):
837
        for dataset in self.data:
838
            yield ('y', dataset)
839
840
841
class GroupedBarChart(BarChart):
842
843
    def __init__(self, *args, **kwargs):
35 by gak
- Initial "grammar" code
844
        if type(self) == GroupedBarChart:
845
            raise AbstractClassException('This is an abstract class')
22 by gak
- pygooglechart.py converted to unix line breaks
846
        BarChart.__init__(self, *args, **kwargs)
847
        self.bar_spacing = None
848
        self.group_spacing = None
849
850
    def set_bar_spacing(self, spacing):
851
        """Set spacing between bars in a group."""
852
        self.bar_spacing = spacing
853
854
    def set_group_spacing(self, spacing):
855
        """Set spacing between groups of bars."""
856
        self.group_spacing = spacing
857
25 by gak
Autoscale fixes and refactoring by Grahm Ullrich
858
    def get_url_bits(self, data_class=None):
22 by gak
- pygooglechart.py converted to unix line breaks
859
        # Skip 'BarChart.get_url_bits' and call Chart directly so the parent
860
        # doesn't add "chbh" before we do.
30 by gak
Added zero line to bar charts
861
        url_bits = BarChart.get_url_bits(self, data_class=data_class,
862
            skip_chbh=True)
22 by gak
- pygooglechart.py converted to unix line breaks
863
        if self.group_spacing is not None:
864
            if self.bar_spacing is None:
33 by gak
pep8 fixes, version bump
865
                raise InvalidParametersException('Bar spacing is required ' \
866
                    'to be set when setting group spacing')
22 by gak
- pygooglechart.py converted to unix line breaks
867
            if self.bar_width is None:
868
                raise InvalidParametersException('Bar width is required to ' \
869
                    'be set when setting bar spacing')
870
            url_bits.append('chbh=%i,%i,%i'
871
                % (self.bar_width, self.bar_spacing, self.group_spacing))
872
        elif self.bar_spacing is not None:
873
            if self.bar_width is None:
874
                raise InvalidParametersException('Bar width is required to ' \
875
                    'be set when setting bar spacing')
876
            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
877
        elif self.bar_width:
22 by gak
- pygooglechart.py converted to unix line breaks
878
            url_bits.append('chbh=%i' % self.bar_width)
879
        return url_bits
880
881
882
class GroupedHorizontalBarChart(GroupedBarChart):
883
884
    def type_to_url(self):
885
        return 'cht=bhg'
886
887
888
class GroupedVerticalBarChart(GroupedBarChart):
889
890
    def type_to_url(self):
891
        return 'cht=bvg'
892
893
    def annotated_data(self):
894
        for dataset in self.data:
895
            yield ('y', dataset)
896
897
898
class PieChart(Chart):
899
900
    def __init__(self, *args, **kwargs):
35 by gak
- Initial "grammar" code
901
        if type(self) == PieChart:
902
            raise AbstractClassException('This is an abstract class')
22 by gak
- pygooglechart.py converted to unix line breaks
903
        Chart.__init__(self, *args, **kwargs)
904
        self.pie_labels = []
36 by gak
- Really added initial unit tests
905
        if self.y_range:
906
            warnings.warn('y_range is not used with %s.' % \
907
                (self.__class__.__name__))
22 by gak
- pygooglechart.py converted to unix line breaks
908
909
    def set_pie_labels(self, labels):
910
        self.pie_labels = [urllib.quote(a) for a in labels]
911
25 by gak
Autoscale fixes and refactoring by Grahm Ullrich
912
    def get_url_bits(self, data_class=None):
913
        url_bits = Chart.get_url_bits(self, data_class=data_class)
22 by gak
- pygooglechart.py converted to unix line breaks
914
        if self.pie_labels:
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
915
            url_bits.append('chl=%s' % '%7c'.join(self.pie_labels))
22 by gak
- pygooglechart.py converted to unix line breaks
916
        return url_bits
917
918
    def annotated_data(self):
919
        # Datasets are all y-axis data. However, there should only be
920
        # one dataset for pie charts.
921
        for dataset in self.data:
36 by gak
- Really added initial unit tests
922
            yield ('x', dataset)
22 by gak
- pygooglechart.py converted to unix line breaks
923
47 by gak
- Fixed bug with automatic scaling and pie charts
924
    def scaled_data(self, data_class, x_range=None, y_range=None):
925
        if not x_range:
926
            x_range = [0, sum(self.data[0])]
927
        return Chart.scaled_data(self, data_class, x_range, self.y_range)
928
22 by gak
- pygooglechart.py converted to unix line breaks
929
930
class PieChart2D(PieChart):
931
932
    def type_to_url(self):
933
        return 'cht=p'
934
935
936
class PieChart3D(PieChart):
937
938
    def type_to_url(self):
939
        return 'cht=p3'
940
941
942
class VennChart(Chart):
943
944
    def type_to_url(self):
945
        return 'cht=v'
946
947
    def annotated_data(self):
948
        for dataset in self.data:
949
            yield ('y', dataset)
950
951
26 by gak
Initial radar chart implementation
952
class RadarChart(Chart):
953
954
    def type_to_url(self):
955
        return 'cht=r'
956
33 by gak
pep8 fixes, version bump
957
28 by gak
Added map chart type
958
class SplineRadarChart(RadarChart):
959
960
    def type_to_url(self):
961
        return 'cht=rs'
962
963
964
class MapChart(Chart):
965
966
    def __init__(self, *args, **kwargs):
967
        Chart.__init__(self, *args, **kwargs)
968
        self.geo_area = 'world'
969
        self.codes = []
58 by gak
- Version bump to 0.3.0
970
        self.__areas = ('africa', 'asia', 'europe', 'middle_east',
971
            'south_america', 'usa', 'world')
972
        self.__ccodes = (
973
            'AD', 'AE', 'AF', 'AG', 'AI', 'AL', 'AM', 'AN', 'AO', 'AQ', 'AR',
974
            'AS', 'AT', 'AU', 'AW', 'AX', 'AZ', 'BA', 'BB', 'BD', 'BE', 'BF',
975
            'BG', 'BH', 'BI', 'BJ', 'BL', 'BM', 'BN', 'BO', 'BR', 'BS', 'BT',
976
            'BV', 'BW', 'BY', 'BZ', 'CA', 'CC', 'CD', 'CF', 'CG', 'CH', 'CI',
977
            'CK', 'CL', 'CM', 'CN', 'CO', 'CR', 'CU', 'CV', 'CX', 'CY', 'CZ',
978
            'DE', 'DJ', 'DK', 'DM', 'DO', 'DZ', 'EC', 'EE', 'EG', 'EH', 'ER',
979
            'ES', 'ET', 'FI', 'FJ', 'FK', 'FM', 'FO', 'FR', 'GA', 'GB', 'GD',
980
            'GE', 'GF', 'GG', 'GH', 'GI', 'GL', 'GM', 'GN', 'GP', 'GQ', 'GR',
981
            'GS', 'GT', 'GU', 'GW', 'GY', 'HK', 'HM', 'HN', 'HR', 'HT', 'HU',
982
            'ID', 'IE', 'IL', 'IM', 'IN', 'IO', 'IQ', 'IR', 'IS', 'IT', 'JE',
983
            'JM', 'JO', 'JP', 'KE', 'KG', 'KH', 'KI', 'KM', 'KN', 'KP', 'KR',
984
            'KW', 'KY', 'KZ', 'LA', 'LB', 'LC', 'LI', 'LK', 'LR', 'LS', 'LT',
985
            'LU', 'LV', 'LY', 'MA', 'MC', 'MD', 'ME', 'MF', 'MG', 'MH', 'MK',
986
            'ML', 'MM', 'MN', 'MO', 'MP', 'MQ', 'MR', 'MS', 'MT', 'MU', 'MV',
987
            'MW', 'MX', 'MY', 'MZ', 'NA', 'NC', 'NE', 'NF', 'NG', 'NI', 'NL',
988
            'NO', 'NP', 'NR', 'NU', 'NZ', 'OM', 'PA', 'PE', 'PF', 'PG', 'PH',
989
            'PK', 'PL', 'PM', 'PN', 'PR', 'PS', 'PT', 'PW', 'PY', 'QA', 'RE',
990
            'RO', 'RS', 'RU', 'RW', 'SA', 'SB', 'SC', 'SD', 'SE', 'SG', 'SH',
991
            'SI', 'SJ', 'SK', 'SL', 'SM', 'SN', 'SO', 'SR', 'ST', 'SV', 'SY',
992
            'SZ', 'TC', 'TD', 'TF', 'TG', 'TH', 'TJ', 'TK', 'TL', 'TM', 'TN',
993
            'TO', 'TR', 'TT', 'TV', 'TW', 'TZ', 'UA', 'UG', 'UM', 'US', 'UY',
994
            'UZ', 'VA', 'VC', 'VE', 'VG', 'VI', 'VN', 'VU', 'WF', 'WS', 'YE',
995
            'YT', 'ZA', 'ZM', 'ZW')
57 by gak
- #23 MapChart extensions (thanks to Andreas Schawo)
996
        
28 by gak
Added map chart type
997
    def type_to_url(self):
998
        return 'cht=t'
999
1000
    def set_codes(self, codes):
57 by gak
- #23 MapChart extensions (thanks to Andreas Schawo)
1001
        '''Set the country code map for the data.
1002
        Codes given in a list.
1003
1004
        i.e. DE - Germany
1005
             AT - Austria
1006
             US - United States
1007
        '''
1008
1009
        codemap = ''
1010
        
1011
        for cc in codes:
1012
            cc = cc.upper()
1013
            if cc in self.__ccodes:
1014
                codemap += cc
1015
            else:
1016
                raise UnknownCountryCodeException(cc)
1017
            
1018
        self.codes = codemap
1019
1020
    def set_geo_area(self, area):
1021
        '''Sets the geo area for the map.
1022
1023
        * africa
1024
        * asia
1025
        * europe
1026
        * middle_east
1027
        * south_america
1028
        * usa
1029
        * world
1030
        '''
1031
        
1032
        if area in self.__areas:
1033
            self.geo_area = area
1034
        else:
1035
            raise UnknownChartType('Unknown chart type for maps: %s' %area)
28 by gak
Added map chart type
1036
1037
    def get_url_bits(self, data_class=None):
1038
        url_bits = Chart.get_url_bits(self, data_class=data_class)
1039
        url_bits.append('chtm=%s' % self.geo_area)
1040
        if self.codes:
1041
            url_bits.append('chld=%s' % ''.join(self.codes))
1042
        return url_bits
1043
57 by gak
- #23 MapChart extensions (thanks to Andreas Schawo)
1044
    def add_data_dict(self, datadict):
1045
        '''Sets the data and country codes via a dictionary.
1046
1047
        i.e. {'DE': 50, 'GB': 30, 'AT': 70}
1048
        '''
1049
1050
        self.set_codes(datadict.keys())
1051
        self.add_data(datadict.values())
1052
29 by gak
Added Google-o-meter chart
1053
1054
class GoogleOMeterChart(PieChart):
1055
    """Inheriting from PieChart because of similar labeling"""
1056
36 by gak
- Really added initial unit tests
1057
    def __init__(self, *args, **kwargs):
1058
        PieChart.__init__(self, *args, **kwargs)
1059
        if self.auto_scale and not self.x_range:
1060
            warnings.warn('Please specify an x_range with GoogleOMeterChart, '
1061
                'otherwise one arrow will always be at the max.')
1062
29 by gak
Added Google-o-meter chart
1063
    def type_to_url(self):
1064
        return 'cht=gom'
1065
26 by gak
Initial radar chart implementation
1066
38 by gak
- Added support for QR Code chart (#8)
1067
class QRChart(Chart):
1068
1069
    def __init__(self, *args, **kwargs):
1070
        Chart.__init__(self, *args, **kwargs)
1071
        self.encoding = None
1072
        self.ec_level = None
1073
        self.margin = None
1074
1075
    def type_to_url(self):
1076
        return 'cht=qr'
1077
1078
    def data_to_url(self, data_class=None):
1079
        if not self.data:
1080
            raise NoDataGivenException()
1081
        return 'chl=%s' % urllib.quote(self.data[0])
1082
1083
    def get_url_bits(self, data_class=None):
1084
        url_bits = Chart.get_url_bits(self, data_class=data_class)
1085
        if self.encoding:
1086
            url_bits.append('choe=%s' % self.encoding)
1087
        if self.ec_level:
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
1088
            url_bits.append('chld=%s%%7c%s' % (self.ec_level, self.margin))
38 by gak
- Added support for QR Code chart (#8)
1089
        return url_bits
1090
1091
    def set_encoding(self, encoding):
1092
        self.encoding = encoding
1093
1094
    def set_ec(self, level, margin):
1095
        self.ec_level = level
1096
        self.margin = margin
1097
1098
35 by gak
- Initial "grammar" code
1099
class ChartGrammar(object):
1100
36 by gak
- Really added initial unit tests
1101
    def __init__(self):
1102
        self.grammar = None
1103
        self.chart = None
1104
1105
    def parse(self, grammar):
35 by gak
- Initial "grammar" code
1106
        self.grammar = grammar
1107
        self.chart = self.create_chart_instance()
1108
36 by gak
- Really added initial unit tests
1109
        for attr in self.grammar:
1110
            if attr in ('w', 'h', 'type', 'auto_scale', 'x_range', 'y_range'):
1111
                continue  # These are already parsed in create_chart_instance
1112
            attr_func = 'parse_' + attr
1113
            if not hasattr(self, attr_func):
1114
                warnings.warn('No parser for grammar attribute "%s"' % (attr))
1115
                continue
1116
            getattr(self, attr_func)(grammar[attr])
1117
1118
        return self.chart
1119
1120
    def parse_data(self, data):
1121
        self.chart.data = data
1122
35 by gak
- Initial "grammar" code
1123
    @staticmethod
1124
    def get_possible_chart_types():
1125
        possible_charts = []
36 by gak
- Really added initial unit tests
1126
        for cls_name in globals().keys():
35 by gak
- Initial "grammar" code
1127
            if not cls_name.endswith('Chart'):
1128
                continue
1129
            cls = globals()[cls_name]
1130
            # Check if it is an abstract class
1131
            try:
36 by gak
- Really added initial unit tests
1132
                a = cls(1, 1, auto_scale=False)
1133
                del a
35 by gak
- Initial "grammar" code
1134
            except AbstractClassException:
1135
                continue
1136
            # Strip off "Class"
1137
            possible_charts.append(cls_name[:-5])
1138
        return possible_charts
1139
36 by gak
- Really added initial unit tests
1140
    def create_chart_instance(self, grammar=None):
1141
        if not grammar:
1142
            grammar = self.grammar
1143
        assert(isinstance(grammar, dict))  # grammar must be a dict
35 by gak
- Initial "grammar" code
1144
        assert('w' in grammar)  # width is required
1145
        assert('h' in grammar)  # height is required
1146
        assert('type' in grammar)  # type is required
36 by gak
- Really added initial unit tests
1147
        chart_type = grammar['type']
1148
        w = grammar['w']
1149
        h = grammar['h']
1150
        auto_scale = grammar.get('auto_scale', None)
1151
        x_range = grammar.get('x_range', None)
1152
        y_range = grammar.get('y_range', None)
35 by gak
- Initial "grammar" code
1153
        types = ChartGrammar.get_possible_chart_types()
36 by gak
- Really added initial unit tests
1154
        if chart_type not in types:
35 by gak
- Initial "grammar" code
1155
            raise UnknownChartType('%s is an unknown chart type. Possible '
36 by gak
- Really added initial unit tests
1156
                'chart types are %s' % (chart_type, ','.join(types)))
1157
        return globals()[chart_type + 'Chart'](w, h, auto_scale=auto_scale,
1158
            x_range=x_range, y_range=y_range)
35 by gak
- Initial "grammar" code
1159
1160
    def download(self):
1161
        pass
1162