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