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