/+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
34 by gak
- initial unit tests
6
Copyright 2007-2008 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
"""
22
23
import os
24
import urllib
25
import urllib2
26
import math
27
import random
28
import re
36 by gak
- Really added initial unit tests
29
import warnings
30
import copy
22 by gak
- pygooglechart.py converted to unix line breaks
31
32
# Helper variables and functions
33
# -----------------------------------------------------------------------------
34
33 by gak
pep8 fixes, version bump
35
__version__ = '0.2.1'
35 by gak
- Initial "grammar" code
36
__author__ = 'Gerald Kaszuba'
22 by gak
- pygooglechart.py converted to unix line breaks
37
38
reo_colour = re.compile('^([A-Fa-f0-9]{2,2}){3,4}$')
39
40
def _check_colour(colour):
41
    if not reo_colour.match(colour):
42
        raise InvalidParametersException('Colours need to be in ' \
43
            'RRGGBB or RRGGBBAA format. One of your colours has %s' % \
44
            colour)
45
36 by gak
- Really added initial unit tests
46
47
def _reset_warnings():
48
    """Helper function to reset all warnings. Used by the unit tests."""
49
    globals()['__warningregistry__'] = None
50
51
22 by gak
- pygooglechart.py converted to unix line breaks
52
# Exception Classes
53
# -----------------------------------------------------------------------------
54
55
56
class PyGoogleChartException(Exception):
57
    pass
58
59
60
class DataOutOfRangeException(PyGoogleChartException):
61
    pass
62
63
64
class UnknownDataTypeException(PyGoogleChartException):
65
    pass
66
67
68
class NoDataGivenException(PyGoogleChartException):
69
    pass
70
71
72
class InvalidParametersException(PyGoogleChartException):
73
    pass
74
75
76
class BadContentTypeException(PyGoogleChartException):
77
    pass
78
79
35 by gak
- Initial "grammar" code
80
class AbstractClassException(PyGoogleChartException):
81
    pass
82
83
84
class UnknownChartType(PyGoogleChartException):
85
    pass
86
87
22 by gak
- pygooglechart.py converted to unix line breaks
88
# Data Classes
89
# -----------------------------------------------------------------------------
90
91
92
class Data(object):
93
94
    def __init__(self, data):
35 by gak
- Initial "grammar" code
95
        if type(self) == Data:
96
            raise AbstractClassException('This is an abstract class')
22 by gak
- pygooglechart.py converted to unix line breaks
97
        self.data = data
98
25 by gak
Autoscale fixes and refactoring by Grahm Ullrich
99
    @classmethod
100
    def float_scale_value(cls, value, range):
101
        lower, upper = range
34 by gak
- initial unit tests
102
        assert(upper > lower)
36 by gak
- Really added initial unit tests
103
        scaled = (value - lower) * (float(cls.max_value) / (upper - lower))
25 by gak
Autoscale fixes and refactoring by Grahm Ullrich
104
        return scaled
105
106
    @classmethod
107
    def clip_value(cls, value):
36 by gak
- Really added initial unit tests
108
        return max(0, min(value, cls.max_value))
25 by gak
Autoscale fixes and refactoring by Grahm Ullrich
109
110
    @classmethod
111
    def int_scale_value(cls, value, range):
34 by gak
- initial unit tests
112
        return int(round(cls.float_scale_value(value, range)))
25 by gak
Autoscale fixes and refactoring by Grahm Ullrich
113
114
    @classmethod
115
    def scale_value(cls, value, range):
116
        scaled = cls.int_scale_value(value, range)
117
        clipped = cls.clip_value(scaled)
37 by gak
- Added "colours within series" option to chart. (chco=xxx|xxx) (Steve Brandt)
118
        Data.check_clip(scaled, clipped)
119
        return clipped
120
121
    @staticmethod
122
    def check_clip(scaled, clipped):
36 by gak
- Really added initial unit tests
123
        if clipped != scaled:
124
            warnings.warn('One or more of of your data points has been '
125
                'clipped because it is out of range.')
22 by gak
- pygooglechart.py converted to unix line breaks
126
33 by gak
pep8 fixes, version bump
127
22 by gak
- pygooglechart.py converted to unix line breaks
128
class SimpleData(Data):
33 by gak
pep8 fixes, version bump
129
36 by gak
- Really added initial unit tests
130
    max_value = 61
22 by gak
- pygooglechart.py converted to unix line breaks
131
    enc_map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
132
133
    def __repr__(self):
134
        encoded_data = []
135
        for data in self.data:
136
            sub_data = []
137
            for value in data:
138
                if value is None:
139
                    sub_data.append('_')
36 by gak
- Really added initial unit tests
140
                elif value >= 0 and value <= self.max_value:
22 by gak
- pygooglechart.py converted to unix line breaks
141
                    sub_data.append(SimpleData.enc_map[value])
142
                else:
143
                    raise DataOutOfRangeException('cannot encode value: %d'
144
                                                  % value)
145
            encoded_data.append(''.join(sub_data))
146
        return 'chd=s:' + ','.join(encoded_data)
147
33 by gak
pep8 fixes, version bump
148
22 by gak
- pygooglechart.py converted to unix line breaks
149
class TextData(Data):
150
36 by gak
- Really added initial unit tests
151
    max_value = 100
152
22 by gak
- pygooglechart.py converted to unix line breaks
153
    def __repr__(self):
154
        encoded_data = []
155
        for data in self.data:
156
            sub_data = []
157
            for value in data:
158
                if value is None:
159
                    sub_data.append(-1)
36 by gak
- Really added initial unit tests
160
                elif value >= 0 and value <= self.max_value:
22 by gak
- pygooglechart.py converted to unix line breaks
161
                    sub_data.append("%.1f" % float(value))
162
                else:
163
                    raise DataOutOfRangeException()
164
            encoded_data.append(','.join(sub_data))
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
165
        return 'chd=t:' + '%7c'.join(encoded_data)
22 by gak
- pygooglechart.py converted to unix line breaks
166
167
    @classmethod
168
    def scale_value(cls, value, range):
25 by gak
Autoscale fixes and refactoring by Grahm Ullrich
169
        # use float values instead of integers because we don't need an encode
170
        # map index
33 by gak
pep8 fixes, version bump
171
        scaled = cls.float_scale_value(value, range)
25 by gak
Autoscale fixes and refactoring by Grahm Ullrich
172
        clipped = cls.clip_value(scaled)
37 by gak
- Added "colours within series" option to chart. (chco=xxx|xxx) (Steve Brandt)
173
        Data.check_clip(scaled, clipped)
25 by gak
Autoscale fixes and refactoring by Grahm Ullrich
174
        return clipped
22 by gak
- pygooglechart.py converted to unix line breaks
175
33 by gak
pep8 fixes, version bump
176
22 by gak
- pygooglechart.py converted to unix line breaks
177
class ExtendedData(Data):
34 by gak
- initial unit tests
178
36 by gak
- Really added initial unit tests
179
    max_value = 4095
22 by gak
- pygooglechart.py converted to unix line breaks
180
    enc_map = \
181
        'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-.'
182
183
    def __repr__(self):
184
        encoded_data = []
185
        enc_size = len(ExtendedData.enc_map)
186
        for data in self.data:
187
            sub_data = []
188
            for value in data:
189
                if value is None:
190
                    sub_data.append('__')
36 by gak
- Really added initial unit tests
191
                elif value >= 0 and value <= self.max_value:
22 by gak
- pygooglechart.py converted to unix line breaks
192
                    first, second = divmod(int(value), enc_size)
193
                    sub_data.append('%s%s' % (
194
                        ExtendedData.enc_map[first],
195
                        ExtendedData.enc_map[second]))
196
                else:
197
                    raise DataOutOfRangeException( \
198
                        'Item #%i "%s" is out of range' % (data.index(value), \
199
                        value))
200
            encoded_data.append(''.join(sub_data))
201
        return 'chd=e:' + ','.join(encoded_data)
202
203
204
# Axis Classes
205
# -----------------------------------------------------------------------------
206
207
208
class Axis(object):
34 by gak
- initial unit tests
209
22 by gak
- pygooglechart.py converted to unix line breaks
210
    BOTTOM = 'x'
211
    TOP = 't'
212
    LEFT = 'y'
213
    RIGHT = 'r'
214
    TYPES = (BOTTOM, TOP, LEFT, RIGHT)
215
216
    def __init__(self, axis_index, axis_type, **kw):
217
        assert(axis_type in Axis.TYPES)
218
        self.has_style = False
219
        self.axis_index = axis_index
220
        self.axis_type = axis_type
221
        self.positions = None
222
223
    def set_index(self, axis_index):
224
        self.axis_index = axis_index
225
226
    def set_positions(self, positions):
227
        self.positions = positions
228
229
    def set_style(self, colour, font_size=None, alignment=None):
230
        _check_colour(colour)
231
        self.colour = colour
232
        self.font_size = font_size
233
        self.alignment = alignment
234
        self.has_style = True
235
236
    def style_to_url(self):
237
        bits = []
238
        bits.append(str(self.axis_index))
239
        bits.append(self.colour)
240
        if self.font_size is not None:
241
            bits.append(str(self.font_size))
242
            if self.alignment is not None:
243
                bits.append(str(self.alignment))
244
        return ','.join(bits)
245
246
    def positions_to_url(self):
247
        bits = []
248
        bits.append(str(self.axis_index))
249
        bits += [str(a) for a in self.positions]
250
        return ','.join(bits)
251
252
253
class LabelAxis(Axis):
254
255
    def __init__(self, axis_index, axis_type, values, **kwargs):
256
        Axis.__init__(self, axis_index, axis_type, **kwargs)
257
        self.values = [str(a) for a in values]
258
259
    def __repr__(self):
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
260
        return '%i:%%7c%s' % (self.axis_index, '%7c'.join(self.values))
22 by gak
- pygooglechart.py converted to unix line breaks
261
262
263
class RangeAxis(Axis):
264
265
    def __init__(self, axis_index, axis_type, low, high, **kwargs):
266
        Axis.__init__(self, axis_index, axis_type, **kwargs)
267
        self.low = low
268
        self.high = high
269
270
    def __repr__(self):
271
        return '%i,%s,%s' % (self.axis_index, self.low, self.high)
272
273
# Chart Classes
274
# -----------------------------------------------------------------------------
275
276
277
class Chart(object):
278
    """Abstract class for all chart types.
279
280
    width are height specify the dimensions of the image. title sets the title
281
    of the chart. legend requires a list that corresponds to datasets.
282
    """
283
284
    BASE_URL = 'http://chart.apis.google.com/chart?'
285
    BACKGROUND = 'bg'
286
    CHART = 'c'
30 by gak
Added zero line to bar charts
287
    ALPHA = 'a'
288
    VALID_SOLID_FILL_TYPES = (BACKGROUND, CHART, ALPHA)
22 by gak
- pygooglechart.py converted to unix line breaks
289
    SOLID = 's'
290
    LINEAR_GRADIENT = 'lg'
291
    LINEAR_STRIPES = 'ls'
292
293
    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)
294
            auto_scale=True, x_range=None, y_range=None,
295
            colours_within_series=None):
35 by gak
- Initial "grammar" code
296
        if type(self) == Chart:
297
            raise AbstractClassException('This is an abstract class')
22 by gak
- pygooglechart.py converted to unix line breaks
298
        assert(isinstance(width, int))
299
        assert(isinstance(height, int))
300
        self.width = width
301
        self.height = height
302
        self.data = []
303
        self.set_title(title)
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
304
        self.set_title_style(None, None)
22 by gak
- pygooglechart.py converted to unix line breaks
305
        self.set_legend(legend)
37 by gak
- Added "colours within series" option to chart. (chco=xxx|xxx) (Steve Brandt)
306
        self.set_legend_position(None)
22 by gak
- pygooglechart.py converted to unix line breaks
307
        self.set_colours(colours)
37 by gak
- Added "colours within series" option to chart. (chco=xxx|xxx) (Steve Brandt)
308
        self.set_colours_within_series(colours_within_series)
22 by gak
- pygooglechart.py converted to unix line breaks
309
310
        # Data for scaling.
36 by gak
- Really added initial unit tests
311
        self.auto_scale = auto_scale  # Whether to automatically scale data
312
        self.x_range = x_range  # (min, max) x-axis range for scaling
313
        self.y_range = y_range  # (min, max) y-axis range for scaling
22 by gak
- pygooglechart.py converted to unix line breaks
314
        self.scaled_data_class = None
315
        self.scaled_x_range = None
316
        self.scaled_y_range = None
317
318
        self.fill_types = {
319
            Chart.BACKGROUND: None,
320
            Chart.CHART: None,
30 by gak
Added zero line to bar charts
321
            Chart.ALPHA: None,
22 by gak
- pygooglechart.py converted to unix line breaks
322
        }
323
        self.fill_area = {
324
            Chart.BACKGROUND: None,
325
            Chart.CHART: None,
30 by gak
Added zero line to bar charts
326
            Chart.ALPHA: None,
22 by gak
- pygooglechart.py converted to unix line breaks
327
        }
328
        self.axis = []
329
        self.markers = []
27 by gak
grids and line styles are not only restricted to line chart types
330
        self.line_styles = {}
331
        self.grid = None
22 by gak
- pygooglechart.py converted to unix line breaks
332
333
    # URL generation
334
    # -------------------------------------------------------------------------
335
25 by gak
Autoscale fixes and refactoring by Grahm Ullrich
336
    def get_url(self, data_class=None):
337
        url_bits = self.get_url_bits(data_class=data_class)
22 by gak
- pygooglechart.py converted to unix line breaks
338
        return self.BASE_URL + '&'.join(url_bits)
339
25 by gak
Autoscale fixes and refactoring by Grahm Ullrich
340
    def get_url_bits(self, data_class=None):
22 by gak
- pygooglechart.py converted to unix line breaks
341
        url_bits = []
342
        # required arguments
343
        url_bits.append(self.type_to_url())
344
        url_bits.append('chs=%ix%i' % (self.width, self.height))
25 by gak
Autoscale fixes and refactoring by Grahm Ullrich
345
        url_bits.append(self.data_to_url(data_class=data_class))
22 by gak
- pygooglechart.py converted to unix line breaks
346
        # optional arguments
347
        if self.title:
348
            url_bits.append('chtt=%s' % self.title)
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
349
        if self.title_colour and self.title_font_size:
350
            url_bits.append('chts=%s,%s' % (self.title_colour, \
351
                self.title_font_size))
22 by gak
- pygooglechart.py converted to unix line breaks
352
        if self.legend:
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
353
            url_bits.append('chdl=%s' % '%7c'.join(self.legend))
37 by gak
- Added "colours within series" option to chart. (chco=xxx|xxx) (Steve Brandt)
354
        if self.legend_position:
355
            url_bits.append('chdlp=%s' % (self.legend_position))
22 by gak
- pygooglechart.py converted to unix line breaks
356
        if self.colours:
37 by gak
- Added "colours within series" option to chart. (chco=xxx|xxx) (Steve Brandt)
357
            url_bits.append('chco=%s' % ','.join(self.colours))            
358
        if self.colours_within_series:
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
359
            url_bits.append('chco=%s' % '%7c'.join(self.colours_within_series))
22 by gak
- pygooglechart.py converted to unix line breaks
360
        ret = self.fill_to_url()
361
        if ret:
362
            url_bits.append(ret)
363
        ret = self.axis_to_url()
364
        if ret:
37 by gak
- Added "colours within series" option to chart. (chco=xxx|xxx) (Steve Brandt)
365
            url_bits.append(ret)                    
22 by gak
- pygooglechart.py converted to unix line breaks
366
        if self.markers:
37 by gak
- Added "colours within series" option to chart. (chco=xxx|xxx) (Steve Brandt)
367
            url_bits.append(self.markers_to_url())        
27 by gak
grids and line styles are not only restricted to line chart types
368
        if self.line_styles:
369
            style = []
370
            for index in xrange(max(self.line_styles) + 1):
371
                if index in self.line_styles:
372
                    values = self.line_styles[index]
373
                else:
374
                    values = ('1', )
375
                style.append(','.join(values))
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
376
            url_bits.append('chls=%s' % '%7c'.join(style))
27 by gak
grids and line styles are not only restricted to line chart types
377
        if self.grid:
378
            url_bits.append('chg=%s' % self.grid)
22 by gak
- pygooglechart.py converted to unix line breaks
379
        return url_bits
380
381
    # Downloading
382
    # -------------------------------------------------------------------------
383
384
    def download(self, file_name):
385
        opener = urllib2.urlopen(self.get_url())
386
387
        if opener.headers['content-type'] != 'image/png':
388
            raise BadContentTypeException('Server responded with a ' \
389
                'content-type of %s' % opener.headers['content-type'])
390
41 by gak
Fixed bug where the module would download twice (#7) (Evan Lezar)
391
        open(file_name, 'wb').write(opener.read())
22 by gak
- pygooglechart.py converted to unix line breaks
392
393
    # Simple settings
394
    # -------------------------------------------------------------------------
395
396
    def set_title(self, title):
397
        if title:
398
            self.title = urllib.quote(title)
399
        else:
400
            self.title = None
401
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
402
    def set_title_style(self, colour, font_size):
403
        if not colour is None:
404
            _check_colour(colour)
405
        self.title_colour = colour
406
        self.title_font_size = font_size
407
22 by gak
- pygooglechart.py converted to unix line breaks
408
    def set_legend(self, legend):
409
        """legend needs to be a list, tuple or None"""
410
        assert(isinstance(legend, list) or isinstance(legend, tuple) or
411
            legend is None)
412
        if legend:
413
            self.legend = [urllib.quote(a) for a in legend]
414
        else:
415
            self.legend = None
416
37 by gak
- Added "colours within series" option to chart. (chco=xxx|xxx) (Steve Brandt)
417
    def set_legend_position(self, legend_position):
418
        if legend_position:
419
            self.legend_position = urllib.quote(legend_position)
420
        else:    
421
            self.legend_position = None
422
22 by gak
- pygooglechart.py converted to unix line breaks
423
    # Chart colours
424
    # -------------------------------------------------------------------------
425
426
    def set_colours(self, colours):
427
        # colours needs to be a list, tuple or None
428
        assert(isinstance(colours, list) or isinstance(colours, tuple) or
429
            colours is None)
430
        # make sure the colours are in the right format
431
        if colours:
432
            for col in colours:
433
                _check_colour(col)
434
        self.colours = colours
435
37 by gak
- Added "colours within series" option to chart. (chco=xxx|xxx) (Steve Brandt)
436
    def set_colours_within_series(self, colours):
437
        # colours needs to be a list, tuple or None
438
        assert(isinstance(colours, list) or isinstance(colours, tuple) or
439
            colours is None)
440
        # make sure the colours are in the right format
441
        if colours:
442
            for col in colours:
443
                _check_colour(col)
444
        self.colours_within_series = colours        
445
22 by gak
- pygooglechart.py converted to unix line breaks
446
    # Background/Chart colours
447
    # -------------------------------------------------------------------------
448
449
    def fill_solid(self, area, colour):
30 by gak
Added zero line to bar charts
450
        assert(area in Chart.VALID_SOLID_FILL_TYPES)
22 by gak
- pygooglechart.py converted to unix line breaks
451
        _check_colour(colour)
452
        self.fill_area[area] = colour
453
        self.fill_types[area] = Chart.SOLID
454
455
    def _check_fill_linear(self, angle, *args):
456
        assert(isinstance(args, list) or isinstance(args, tuple))
457
        assert(angle >= 0 and angle <= 90)
458
        assert(len(args) % 2 == 0)
459
        args = list(args)  # args is probably a tuple and we need to mutate
460
        for a in xrange(len(args) / 2):
461
            col = args[a * 2]
462
            offset = args[a * 2 + 1]
463
            _check_colour(col)
464
            assert(offset >= 0 and offset <= 1)
465
            args[a * 2 + 1] = str(args[a * 2 + 1])
466
        return args
467
468
    def fill_linear_gradient(self, area, angle, *args):
30 by gak
Added zero line to bar charts
469
        assert(area in Chart.VALID_SOLID_FILL_TYPES)
22 by gak
- pygooglechart.py converted to unix line breaks
470
        args = self._check_fill_linear(angle, *args)
471
        self.fill_types[area] = Chart.LINEAR_GRADIENT
472
        self.fill_area[area] = ','.join([str(angle)] + args)
473
474
    def fill_linear_stripes(self, area, angle, *args):
30 by gak
Added zero line to bar charts
475
        assert(area in Chart.VALID_SOLID_FILL_TYPES)
22 by gak
- pygooglechart.py converted to unix line breaks
476
        args = self._check_fill_linear(angle, *args)
477
        self.fill_types[area] = Chart.LINEAR_STRIPES
478
        self.fill_area[area] = ','.join([str(angle)] + args)
479
480
    def fill_to_url(self):
481
        areas = []
30 by gak
Added zero line to bar charts
482
        for area in (Chart.BACKGROUND, Chart.CHART, Chart.ALPHA):
22 by gak
- pygooglechart.py converted to unix line breaks
483
            if self.fill_types[area]:
484
                areas.append('%s,%s,%s' % (area, self.fill_types[area], \
485
                    self.fill_area[area]))
486
        if areas:
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
487
            return 'chf=' + '%7c'.join(areas)
22 by gak
- pygooglechart.py converted to unix line breaks
488
489
    # Data
490
    # -------------------------------------------------------------------------
491
492
    def data_class_detection(self, data):
493
        """Determines the appropriate data encoding type to give satisfactory
494
        resolution (http://code.google.com/apis/chart/#chart_data).
495
        """
496
        assert(isinstance(data, list) or isinstance(data, tuple))
497
        if not isinstance(self, (LineChart, BarChart, ScatterChart)):
498
            # From the link above:
499
            #   Simple encoding is suitable for all other types of chart
500
            #   regardless of size.
501
            return SimpleData
502
        elif self.height < 100:
503
            # The link above indicates that line and bar charts less
504
            # than 300px in size can be suitably represented with the
505
            # simple encoding. I've found that this isn't sufficient,
506
            # e.g. examples/line-xy-circle.png. Let's try 100px.
507
            return SimpleData
508
        else:
509
            return ExtendedData
510
39 by gak
- Bug fixed when automatic scaling is on and None values are in a data set (#5) (Alec Thomas)
511
    def _filter_none(self, data):
512
        return [r for r in data if r is not None]
513
22 by gak
- pygooglechart.py converted to unix line breaks
514
    def data_x_range(self):
515
        """Return a 2-tuple giving the minimum and maximum x-axis
516
        data range.
517
        """
518
        try:
39 by gak
- Bug fixed when automatic scaling is on and None values are in a data set (#5) (Alec Thomas)
519
            lower = min([min(self._filter_none(s))
520
                         for type, s in self.annotated_data()
22 by gak
- pygooglechart.py converted to unix line breaks
521
                         if type == 'x'])
39 by gak
- Bug fixed when automatic scaling is on and None values are in a data set (#5) (Alec Thomas)
522
            upper = max([max(self._filter_none(s))
523
                         for type, s in self.annotated_data()
22 by gak
- pygooglechart.py converted to unix line breaks
524
                         if type == 'x'])
525
            return (lower, upper)
526
        except ValueError:
527
            return None     # no x-axis datasets
528
529
    def data_y_range(self):
530
        """Return a 2-tuple giving the minimum and maximum y-axis
531
        data range.
532
        """
533
        try:
39 by gak
- Bug fixed when automatic scaling is on and None values are in a data set (#5) (Alec Thomas)
534
            lower = min([min(self._filter_none(s))
535
                         for type, s in self.annotated_data()
22 by gak
- pygooglechart.py converted to unix line breaks
536
                         if type == 'y'])
39 by gak
- Bug fixed when automatic scaling is on and None values are in a data set (#5) (Alec Thomas)
537
            upper = max([max(self._filter_none(s)) + 1
538
                         for type, s in self.annotated_data()
22 by gak
- pygooglechart.py converted to unix line breaks
539
                         if type == 'y'])
540
            return (lower, upper)
541
        except ValueError:
542
            return None     # no y-axis datasets
543
544
    def scaled_data(self, data_class, x_range=None, y_range=None):
545
        """Scale `self.data` as appropriate for the given data encoding
546
        (data_class) and return it.
547
548
        An optional `y_range` -- a 2-tuple (lower, upper) -- can be
549
        given to specify the y-axis bounds. If not given, the range is
550
        inferred from the data: (0, <max-value>) presuming no negative
551
        values, or (<min-value>, <max-value>) if there are negative
552
        values.  `self.scaled_y_range` is set to the actual lower and
553
        upper scaling range.
554
555
        Ditto for `x_range`. Note that some chart types don't have x-axis
556
        data.
557
        """
558
        self.scaled_data_class = data_class
559
560
        # Determine the x-axis range for scaling.
561
        if x_range is None:
562
            x_range = self.data_x_range()
563
            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)
564
                x_range = (x_range[0], x_range[1])
22 by gak
- pygooglechart.py converted to unix line breaks
565
        self.scaled_x_range = x_range
566
567
        # Determine the y-axis range for scaling.
568
        if y_range is None:
569
            y_range = self.data_y_range()
570
            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)
571
                y_range = (y_range[0], y_range[1])
22 by gak
- pygooglechart.py converted to unix line breaks
572
        self.scaled_y_range = y_range
573
574
        scaled_data = []
575
        for type, dataset in self.annotated_data():
576
            if type == 'x':
577
                scale_range = x_range
578
            elif type == 'y':
579
                scale_range = y_range
580
            elif type == 'marker-size':
581
                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)
582
            scaled_dataset = []
583
            for v in dataset:
584
                if v is None:
585
                    scaled_dataset.append(None)
586
                else:
587
                    scaled_dataset.append(
588
                        data_class.scale_value(v, scale_range))
589
            scaled_data.append(scaled_dataset)
22 by gak
- pygooglechart.py converted to unix line breaks
590
        return scaled_data
591
592
    def add_data(self, data):
593
        self.data.append(data)
594
        return len(self.data) - 1  # return the "index" of the data set
595
596
    def data_to_url(self, data_class=None):
597
        if not data_class:
598
            data_class = self.data_class_detection(self.data)
599
        if not issubclass(data_class, Data):
600
            raise UnknownDataTypeException()
601
        if self.auto_scale:
602
            data = self.scaled_data(data_class, self.x_range, self.y_range)
603
        else:
604
            data = self.data
605
        return repr(data_class(data))
606
29 by gak
Added Google-o-meter chart
607
    def annotated_data(self):
608
        for dataset in self.data:
609
            yield ('x', dataset)
610
22 by gak
- pygooglechart.py converted to unix line breaks
611
    # Axis Labels
612
    # -------------------------------------------------------------------------
613
614
    def set_axis_labels(self, axis_type, values):
615
        assert(axis_type in Axis.TYPES)
47 by gak
- Fixed bug with automatic scaling and pie charts
616
        values = [urllib.quote(str(a)) for a in values]
22 by gak
- pygooglechart.py converted to unix line breaks
617
        axis_index = len(self.axis)
618
        axis = LabelAxis(axis_index, axis_type, values)
619
        self.axis.append(axis)
620
        return axis_index
621
622
    def set_axis_range(self, axis_type, low, high):
623
        assert(axis_type in Axis.TYPES)
624
        axis_index = len(self.axis)
625
        axis = RangeAxis(axis_index, axis_type, low, high)
626
        self.axis.append(axis)
627
        return axis_index
628
629
    def set_axis_positions(self, axis_index, positions):
630
        try:
631
            self.axis[axis_index].set_positions(positions)
632
        except IndexError:
633
            raise InvalidParametersException('Axis index %i has not been ' \
634
                'created' % axis)
635
636
    def set_axis_style(self, axis_index, colour, font_size=None, \
637
            alignment=None):
638
        try:
639
            self.axis[axis_index].set_style(colour, font_size, alignment)
640
        except IndexError:
641
            raise InvalidParametersException('Axis index %i has not been ' \
642
                'created' % axis)
643
644
    def axis_to_url(self):
645
        available_axis = []
646
        label_axis = []
647
        range_axis = []
648
        positions = []
649
        styles = []
650
        index = -1
651
        for axis in self.axis:
652
            available_axis.append(axis.axis_type)
653
            if isinstance(axis, RangeAxis):
654
                range_axis.append(repr(axis))
655
            if isinstance(axis, LabelAxis):
656
                label_axis.append(repr(axis))
657
            if axis.positions:
658
                positions.append(axis.positions_to_url())
659
            if axis.has_style:
660
                styles.append(axis.style_to_url())
661
        if not available_axis:
662
            return
663
        url_bits = []
664
        url_bits.append('chxt=%s' % ','.join(available_axis))
665
        if label_axis:
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
666
            url_bits.append('chxl=%s' % '%7c'.join(label_axis))
22 by gak
- pygooglechart.py converted to unix line breaks
667
        if range_axis:
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
668
            url_bits.append('chxr=%s' % '%7c'.join(range_axis))
22 by gak
- pygooglechart.py converted to unix line breaks
669
        if positions:
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
670
            url_bits.append('chxp=%s' % '%7c'.join(positions))
22 by gak
- pygooglechart.py converted to unix line breaks
671
        if styles:
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
672
            url_bits.append('chxs=%s' % '%7c'.join(styles))
22 by gak
- pygooglechart.py converted to unix line breaks
673
        return '&'.join(url_bits)
674
675
    # Markers, Ranges and Fill area (chm)
676
    # -------------------------------------------------------------------------
677
37 by gak
- Added "colours within series" option to chart. (chco=xxx|xxx) (Steve Brandt)
678
    def markers_to_url(self):        
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
679
        return 'chm=%s' % '%7c'.join([','.join(a) for a in self.markers])
22 by gak
- pygooglechart.py converted to unix line breaks
680
31 by gak
Added priority to shape markers
681
    def add_marker(self, index, point, marker_type, colour, size, priority=0):
22 by gak
- pygooglechart.py converted to unix line breaks
682
        self.markers.append((marker_type, colour, str(index), str(point), \
31 by gak
Added priority to shape markers
683
            str(size), str(priority)))
22 by gak
- pygooglechart.py converted to unix line breaks
684
685
    def add_horizontal_range(self, colour, start, stop):
37 by gak
- Added "colours within series" option to chart. (chco=xxx|xxx) (Steve Brandt)
686
        self.markers.append(('r', colour, '0', str(start), str(stop)))
687
688
    def add_data_line(self, colour, data_set, size, priority=0):
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
689
        self.markers.append(('D', colour, str(data_set), '0', str(size), \
690
            str(priority)))
37 by gak
- Added "colours within series" option to chart. (chco=xxx|xxx) (Steve Brandt)
691
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
692
    def add_marker_text(self, string, colour, data_set, data_point, size, \
693
            priority=0):
694
        self.markers.append((str(string), colour, str(data_set), \
695
            str(data_point), str(size), str(priority)))        
22 by gak
- pygooglechart.py converted to unix line breaks
696
697
    def add_vertical_range(self, colour, start, stop):
37 by gak
- Added "colours within series" option to chart. (chco=xxx|xxx) (Steve Brandt)
698
        self.markers.append(('R', colour, '0', str(start), str(stop)))
22 by gak
- pygooglechart.py converted to unix line breaks
699
700
    def add_fill_range(self, colour, index_start, index_end):
701
        self.markers.append(('b', colour, str(index_start), str(index_end), \
702
            '1'))
703
704
    def add_fill_simple(self, colour):
705
        self.markers.append(('B', colour, '1', '1', '1'))
706
27 by gak
grids and line styles are not only restricted to line chart types
707
    # Line styles
708
    # -------------------------------------------------------------------------
22 by gak
- pygooglechart.py converted to unix line breaks
709
710
    def set_line_style(self, index, thickness=1, line_segment=None, \
711
            blank_segment=None):
712
        value = []
713
        value.append(str(thickness))
714
        if line_segment:
715
            value.append(str(line_segment))
716
            value.append(str(blank_segment))
717
        self.line_styles[index] = value
718
27 by gak
grids and line styles are not only restricted to line chart types
719
    # Grid
720
    # -------------------------------------------------------------------------
721
22 by gak
- pygooglechart.py converted to unix line breaks
722
    def set_grid(self, x_step, y_step, line_segment=1, \
723
            blank_segment=0):
724
        self.grid = '%s,%s,%s,%s' % (x_step, y_step, line_segment, \
725
            blank_segment)
726
27 by gak
grids and line styles are not only restricted to line chart types
727
728
class ScatterChart(Chart):
729
730
    def type_to_url(self):
731
        return 'cht=s'
732
733
    def annotated_data(self):
734
        yield ('x', self.data[0])
735
        yield ('y', self.data[1])
736
        if len(self.data) > 2:
737
            # The optional third dataset is relative sizing for point
738
            # markers.
739
            yield ('marker-size', self.data[2])
740
33 by gak
pep8 fixes, version bump
741
27 by gak
grids and line styles are not only restricted to line chart types
742
class LineChart(Chart):
743
744
    def __init__(self, *args, **kwargs):
35 by gak
- Initial "grammar" code
745
        if type(self) == LineChart:
746
            raise AbstractClassException('This is an abstract class')
27 by gak
grids and line styles are not only restricted to line chart types
747
        Chart.__init__(self, *args, **kwargs)
748
22 by gak
- pygooglechart.py converted to unix line breaks
749
750
class SimpleLineChart(LineChart):
751
752
    def type_to_url(self):
753
        return 'cht=lc'
754
755
    def annotated_data(self):
756
        # All datasets are y-axis data.
757
        for dataset in self.data:
758
            yield ('y', dataset)
759
33 by gak
pep8 fixes, version bump
760
22 by gak
- pygooglechart.py converted to unix line breaks
761
class SparkLineChart(SimpleLineChart):
762
763
    def type_to_url(self):
764
        return 'cht=ls'
765
33 by gak
pep8 fixes, version bump
766
22 by gak
- pygooglechart.py converted to unix line breaks
767
class XYLineChart(LineChart):
768
769
    def type_to_url(self):
770
        return 'cht=lxy'
771
772
    def annotated_data(self):
773
        # Datasets alternate between x-axis, y-axis.
774
        for i, dataset in enumerate(self.data):
775
            if i % 2 == 0:
776
                yield ('x', dataset)
777
            else:
778
                yield ('y', dataset)
779
33 by gak
pep8 fixes, version bump
780
22 by gak
- pygooglechart.py converted to unix line breaks
781
class BarChart(Chart):
782
783
    def __init__(self, *args, **kwargs):
35 by gak
- Initial "grammar" code
784
        if type(self) == BarChart:
785
            raise AbstractClassException('This is an abstract class')
22 by gak
- pygooglechart.py converted to unix line breaks
786
        Chart.__init__(self, *args, **kwargs)
787
        self.bar_width = None
30 by gak
Added zero line to bar charts
788
        self.zero_lines = {}
22 by gak
- pygooglechart.py converted to unix line breaks
789
790
    def set_bar_width(self, bar_width):
791
        self.bar_width = bar_width
792
30 by gak
Added zero line to bar charts
793
    def set_zero_line(self, index, zero_line):
794
        self.zero_lines[index] = zero_line
795
796
    def get_url_bits(self, data_class=None, skip_chbh=False):
25 by gak
Autoscale fixes and refactoring by Grahm Ullrich
797
        url_bits = Chart.get_url_bits(self, data_class=data_class)
30 by gak
Added zero line to bar charts
798
        if not skip_chbh and self.bar_width is not None:
22 by gak
- pygooglechart.py converted to unix line breaks
799
            url_bits.append('chbh=%i' % self.bar_width)
30 by gak
Added zero line to bar charts
800
        zero_line = []
801
        if self.zero_lines:
802
            for index in xrange(max(self.zero_lines) + 1):
803
                if index in self.zero_lines:
804
                    zero_line.append(str(self.zero_lines[index]))
805
                else:
806
                    zero_line.append('0')
807
            url_bits.append('chp=%s' % ','.join(zero_line))
22 by gak
- pygooglechart.py converted to unix line breaks
808
        return url_bits
809
810
811
class StackedHorizontalBarChart(BarChart):
812
813
    def type_to_url(self):
814
        return 'cht=bhs'
815
816
817
class StackedVerticalBarChart(BarChart):
818
819
    def type_to_url(self):
820
        return 'cht=bvs'
821
822
    def annotated_data(self):
823
        for dataset in self.data:
824
            yield ('y', dataset)
825
826
827
class GroupedBarChart(BarChart):
828
829
    def __init__(self, *args, **kwargs):
35 by gak
- Initial "grammar" code
830
        if type(self) == GroupedBarChart:
831
            raise AbstractClassException('This is an abstract class')
22 by gak
- pygooglechart.py converted to unix line breaks
832
        BarChart.__init__(self, *args, **kwargs)
833
        self.bar_spacing = None
834
        self.group_spacing = None
835
836
    def set_bar_spacing(self, spacing):
837
        """Set spacing between bars in a group."""
838
        self.bar_spacing = spacing
839
840
    def set_group_spacing(self, spacing):
841
        """Set spacing between groups of bars."""
842
        self.group_spacing = spacing
843
25 by gak
Autoscale fixes and refactoring by Grahm Ullrich
844
    def get_url_bits(self, data_class=None):
22 by gak
- pygooglechart.py converted to unix line breaks
845
        # Skip 'BarChart.get_url_bits' and call Chart directly so the parent
846
        # doesn't add "chbh" before we do.
30 by gak
Added zero line to bar charts
847
        url_bits = BarChart.get_url_bits(self, data_class=data_class,
848
            skip_chbh=True)
22 by gak
- pygooglechart.py converted to unix line breaks
849
        if self.group_spacing is not None:
850
            if self.bar_spacing is None:
33 by gak
pep8 fixes, version bump
851
                raise InvalidParametersException('Bar spacing is required ' \
852
                    'to be set when setting group spacing')
22 by gak
- pygooglechart.py converted to unix line breaks
853
            if self.bar_width is None:
854
                raise InvalidParametersException('Bar width is required to ' \
855
                    'be set when setting bar spacing')
856
            url_bits.append('chbh=%i,%i,%i'
857
                % (self.bar_width, self.bar_spacing, self.group_spacing))
858
        elif self.bar_spacing is not None:
859
            if self.bar_width is None:
860
                raise InvalidParametersException('Bar width is required to ' \
861
                    'be set when setting bar spacing')
862
            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
863
        elif self.bar_width:
22 by gak
- pygooglechart.py converted to unix line breaks
864
            url_bits.append('chbh=%i' % self.bar_width)
865
        return url_bits
866
867
868
class GroupedHorizontalBarChart(GroupedBarChart):
869
870
    def type_to_url(self):
871
        return 'cht=bhg'
872
873
874
class GroupedVerticalBarChart(GroupedBarChart):
875
876
    def type_to_url(self):
877
        return 'cht=bvg'
878
879
    def annotated_data(self):
880
        for dataset in self.data:
881
            yield ('y', dataset)
882
883
884
class PieChart(Chart):
885
886
    def __init__(self, *args, **kwargs):
35 by gak
- Initial "grammar" code
887
        if type(self) == PieChart:
888
            raise AbstractClassException('This is an abstract class')
22 by gak
- pygooglechart.py converted to unix line breaks
889
        Chart.__init__(self, *args, **kwargs)
890
        self.pie_labels = []
36 by gak
- Really added initial unit tests
891
        if self.y_range:
892
            warnings.warn('y_range is not used with %s.' % \
893
                (self.__class__.__name__))
22 by gak
- pygooglechart.py converted to unix line breaks
894
895
    def set_pie_labels(self, labels):
896
        self.pie_labels = [urllib.quote(a) for a in labels]
897
25 by gak
Autoscale fixes and refactoring by Grahm Ullrich
898
    def get_url_bits(self, data_class=None):
899
        url_bits = Chart.get_url_bits(self, data_class=data_class)
22 by gak
- pygooglechart.py converted to unix line breaks
900
        if self.pie_labels:
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
901
            url_bits.append('chl=%s' % '%7c'.join(self.pie_labels))
22 by gak
- pygooglechart.py converted to unix line breaks
902
        return url_bits
903
904
    def annotated_data(self):
905
        # Datasets are all y-axis data. However, there should only be
906
        # one dataset for pie charts.
907
        for dataset in self.data:
36 by gak
- Really added initial unit tests
908
            yield ('x', dataset)
22 by gak
- pygooglechart.py converted to unix line breaks
909
47 by gak
- Fixed bug with automatic scaling and pie charts
910
    def scaled_data(self, data_class, x_range=None, y_range=None):
911
        if not x_range:
912
            x_range = [0, sum(self.data[0])]
913
        return Chart.scaled_data(self, data_class, x_range, self.y_range)
914
22 by gak
- pygooglechart.py converted to unix line breaks
915
916
class PieChart2D(PieChart):
917
918
    def type_to_url(self):
919
        return 'cht=p'
920
921
922
class PieChart3D(PieChart):
923
924
    def type_to_url(self):
925
        return 'cht=p3'
926
927
928
class VennChart(Chart):
929
930
    def type_to_url(self):
931
        return 'cht=v'
932
933
    def annotated_data(self):
934
        for dataset in self.data:
935
            yield ('y', dataset)
936
937
26 by gak
Initial radar chart implementation
938
class RadarChart(Chart):
939
940
    def type_to_url(self):
941
        return 'cht=r'
942
33 by gak
pep8 fixes, version bump
943
28 by gak
Added map chart type
944
class SplineRadarChart(RadarChart):
945
946
    def type_to_url(self):
947
        return 'cht=rs'
948
949
950
class MapChart(Chart):
951
952
    def __init__(self, *args, **kwargs):
953
        Chart.__init__(self, *args, **kwargs)
954
        self.geo_area = 'world'
955
        self.codes = []
956
957
    def type_to_url(self):
958
        return 'cht=t'
959
960
    def set_codes(self, codes):
961
        self.codes = codes
962
963
    def get_url_bits(self, data_class=None):
964
        url_bits = Chart.get_url_bits(self, data_class=data_class)
965
        url_bits.append('chtm=%s' % self.geo_area)
966
        if self.codes:
967
            url_bits.append('chld=%s' % ''.join(self.codes))
968
        return url_bits
969
29 by gak
Added Google-o-meter chart
970
971
class GoogleOMeterChart(PieChart):
972
    """Inheriting from PieChart because of similar labeling"""
973
36 by gak
- Really added initial unit tests
974
    def __init__(self, *args, **kwargs):
975
        PieChart.__init__(self, *args, **kwargs)
976
        if self.auto_scale and not self.x_range:
977
            warnings.warn('Please specify an x_range with GoogleOMeterChart, '
978
                'otherwise one arrow will always be at the max.')
979
29 by gak
Added Google-o-meter chart
980
    def type_to_url(self):
981
        return 'cht=gom'
982
26 by gak
Initial radar chart implementation
983
38 by gak
- Added support for QR Code chart (#8)
984
class QRChart(Chart):
985
986
    def __init__(self, *args, **kwargs):
987
        Chart.__init__(self, *args, **kwargs)
988
        self.encoding = None
989
        self.ec_level = None
990
        self.margin = None
991
992
    def type_to_url(self):
993
        return 'cht=qr'
994
995
    def data_to_url(self, data_class=None):
996
        if not self.data:
997
            raise NoDataGivenException()
998
        return 'chl=%s' % urllib.quote(self.data[0])
999
1000
    def get_url_bits(self, data_class=None):
1001
        url_bits = Chart.get_url_bits(self, data_class=data_class)
1002
        if self.encoding:
1003
            url_bits.append('choe=%s' % self.encoding)
1004
        if self.ec_level:
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
1005
            url_bits.append('chld=%s%%7c%s' % (self.ec_level, self.margin))
38 by gak
- Added support for QR Code chart (#8)
1006
        return url_bits
1007
1008
    def set_encoding(self, encoding):
1009
        self.encoding = encoding
1010
1011
    def set_ec(self, level, margin):
1012
        self.ec_level = level
1013
        self.margin = margin
1014
1015
35 by gak
- Initial "grammar" code
1016
class ChartGrammar(object):
1017
36 by gak
- Really added initial unit tests
1018
    def __init__(self):
1019
        self.grammar = None
1020
        self.chart = None
50 by gak
- All pipe "|" characters are now encoded to %7C (Tom Payne)
1021
        warnings.warn('This code is incomplete!')
36 by gak
- Really added initial unit tests
1022
1023
    def parse(self, grammar):
35 by gak
- Initial "grammar" code
1024
        self.grammar = grammar
1025
        self.chart = self.create_chart_instance()
1026
36 by gak
- Really added initial unit tests
1027
        for attr in self.grammar:
1028
            if attr in ('w', 'h', 'type', 'auto_scale', 'x_range', 'y_range'):
1029
                continue  # These are already parsed in create_chart_instance
1030
            attr_func = 'parse_' + attr
1031
            if not hasattr(self, attr_func):
1032
                warnings.warn('No parser for grammar attribute "%s"' % (attr))
1033
                continue
1034
            getattr(self, attr_func)(grammar[attr])
1035
1036
        return self.chart
1037
1038
    def parse_data(self, data):
1039
        self.chart.data = data
1040
35 by gak
- Initial "grammar" code
1041
    @staticmethod
1042
    def get_possible_chart_types():
1043
        possible_charts = []
36 by gak
- Really added initial unit tests
1044
        for cls_name in globals().keys():
35 by gak
- Initial "grammar" code
1045
            if not cls_name.endswith('Chart'):
1046
                continue
1047
            cls = globals()[cls_name]
1048
            # Check if it is an abstract class
1049
            try:
36 by gak
- Really added initial unit tests
1050
                a = cls(1, 1, auto_scale=False)
1051
                del a
35 by gak
- Initial "grammar" code
1052
            except AbstractClassException:
1053
                continue
1054
            # Strip off "Class"
1055
            possible_charts.append(cls_name[:-5])
1056
        return possible_charts
1057
36 by gak
- Really added initial unit tests
1058
    def create_chart_instance(self, grammar=None):
1059
        if not grammar:
1060
            grammar = self.grammar
1061
        assert(isinstance(grammar, dict))  # grammar must be a dict
35 by gak
- Initial "grammar" code
1062
        assert('w' in grammar)  # width is required
1063
        assert('h' in grammar)  # height is required
1064
        assert('type' in grammar)  # type is required
36 by gak
- Really added initial unit tests
1065
        chart_type = grammar['type']
1066
        w = grammar['w']
1067
        h = grammar['h']
1068
        auto_scale = grammar.get('auto_scale', None)
1069
        x_range = grammar.get('x_range', None)
1070
        y_range = grammar.get('y_range', None)
35 by gak
- Initial "grammar" code
1071
        types = ChartGrammar.get_possible_chart_types()
36 by gak
- Really added initial unit tests
1072
        if chart_type not in types:
35 by gak
- Initial "grammar" code
1073
            raise UnknownChartType('%s is an unknown chart type. Possible '
36 by gak
- Really added initial unit tests
1074
                'chart types are %s' % (chart_type, ','.join(types)))
1075
        return globals()[chart_type + 'Chart'](w, h, auto_scale=auto_scale,
1076
            x_range=x_range, y_range=y_range)
35 by gak
- Initial "grammar" code
1077
1078
    def download(self):
1079
        pass
1080