/+junk/pygooglechart-py3k

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/%2Bjunk/pygooglechart-py3k

« back to all changes in this revision

Viewing changes to pygooglechart.py

  • Committer: gak
  • Date: 2007-12-15 23:21:31 UTC
  • Revision ID: git-v1:857495549bac47c6a70872d4b36dd1fe49270357
version bump and MANIFEST fixes to allow examples and COPYING in the sdist

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
"""
2
 
pygooglechart - A complete Python wrapper for the Google Chart API
3
 
 
4
 
http://pygooglechart.slowchop.com/
5
 
 
6
 
Copyright 2007-2008 Gerald Kaszuba
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
 
from __future__ import division
23
 
 
24
 
import os
25
 
import urllib
26
 
import urllib2
27
 
import math
28
 
import random
29
 
import re
30
 
import warnings
31
 
import copy
32
 
 
33
 
# Helper variables and functions
34
 
# -----------------------------------------------------------------------------
35
 
 
36
 
__version__ = '0.2.2'
37
 
__author__ = 'Gerald Kaszuba'
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
 
 
47
 
 
48
 
def _reset_warnings():
49
 
    """Helper function to reset all warnings. Used by the unit tests."""
50
 
    globals()['__warningregistry__'] = None
51
 
 
52
 
 
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
 
 
81
 
class AbstractClassException(PyGoogleChartException):
82
 
    pass
83
 
 
84
 
 
85
 
class UnknownChartType(PyGoogleChartException):
86
 
    pass
87
 
 
88
 
class UnknownCountryCodeException(PyGoogleChartException):
89
 
    pass
90
 
 
91
 
# Data Classes
92
 
# -----------------------------------------------------------------------------
93
 
 
94
 
 
95
 
class Data(object):
96
 
 
97
 
    def __init__(self, data):
98
 
        if type(self) == Data:
99
 
            raise AbstractClassException('This is an abstract class')
100
 
        self.data = data
101
 
 
102
 
    @classmethod
103
 
    def float_scale_value(cls, value, range):
104
 
        lower, upper = range
105
 
        assert(upper > lower)
106
 
        scaled = (value - lower) * (cls.max_value / (upper - lower))
107
 
        return scaled
108
 
 
109
 
    @classmethod
110
 
    def clip_value(cls, value):
111
 
        return max(0, min(value, cls.max_value))
112
 
 
113
 
    @classmethod
114
 
    def int_scale_value(cls, value, range):
115
 
        return int(round(cls.float_scale_value(value, range)))
116
 
 
117
 
    @classmethod
118
 
    def scale_value(cls, value, range):
119
 
        scaled = cls.int_scale_value(value, range)
120
 
        clipped = cls.clip_value(scaled)
121
 
        Data.check_clip(scaled, clipped)
122
 
        return clipped
123
 
 
124
 
    @staticmethod
125
 
    def check_clip(scaled, clipped):
126
 
        if clipped != scaled:
127
 
            warnings.warn('One or more of of your data points has been '
128
 
                'clipped because it is out of range.')
129
 
 
130
 
 
131
 
class SimpleData(Data):
132
 
 
133
 
    max_value = 61
134
 
    enc_map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
135
 
 
136
 
    def __repr__(self):
137
 
        encoded_data = []
138
 
        for data in self.data:
139
 
            sub_data = []
140
 
            for value in data:
141
 
                if value is None:
142
 
                    sub_data.append('_')
143
 
                elif value >= 0 and value <= self.max_value:
144
 
                    sub_data.append(SimpleData.enc_map[value])
145
 
                else:
146
 
                    raise DataOutOfRangeException('cannot encode value: %d'
147
 
                                                  % value)
148
 
            encoded_data.append(''.join(sub_data))
149
 
        return 'chd=s:' + ','.join(encoded_data)
150
 
 
151
 
 
152
 
class TextData(Data):
153
 
 
154
 
    max_value = 100
155
 
 
156
 
    def __repr__(self):
157
 
        encoded_data = []
158
 
        for data in self.data:
159
 
            sub_data = []
160
 
            for value in data:
161
 
                if value is None:
162
 
                    sub_data.append(-1)
163
 
                elif value >= 0 and value <= self.max_value:
164
 
                    sub_data.append("%.1f" % float(value))
165
 
                else:
166
 
                    raise DataOutOfRangeException()
167
 
            encoded_data.append(','.join(sub_data))
168
 
        return 'chd=t:' + '%7c'.join(encoded_data)
169
 
 
170
 
    @classmethod
171
 
    def scale_value(cls, value, range):
172
 
        # use float values instead of integers because we don't need an encode
173
 
        # map index
174
 
        scaled = cls.float_scale_value(value, range)
175
 
        clipped = cls.clip_value(scaled)
176
 
        Data.check_clip(scaled, clipped)
177
 
        return clipped
178
 
 
179
 
 
180
 
class ExtendedData(Data):
181
 
 
182
 
    max_value = 4095
183
 
    enc_map = \
184
 
        'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-.'
185
 
 
186
 
    def __repr__(self):
187
 
        encoded_data = []
188
 
        enc_size = len(ExtendedData.enc_map)
189
 
        for data in self.data:
190
 
            sub_data = []
191
 
            for value in data:
192
 
                if value is None:
193
 
                    sub_data.append('__')
194
 
                elif value >= 0 and value <= self.max_value:
195
 
                    first, second = divmod(int(value), enc_size)
196
 
                    sub_data.append('%s%s' % (
197
 
                        ExtendedData.enc_map[first],
198
 
                        ExtendedData.enc_map[second]))
199
 
                else:
200
 
                    raise DataOutOfRangeException( \
201
 
                        'Item #%i "%s" is out of range' % (data.index(value), \
202
 
                        value))
203
 
            encoded_data.append(''.join(sub_data))
204
 
        return 'chd=e:' + ','.join(encoded_data)
205
 
 
206
 
 
207
 
# Axis Classes
208
 
# -----------------------------------------------------------------------------
209
 
 
210
 
 
211
 
class Axis(object):
212
 
 
213
 
    BOTTOM = 'x'
214
 
    TOP = 't'
215
 
    LEFT = 'y'
216
 
    RIGHT = 'r'
217
 
    TYPES = (BOTTOM, TOP, LEFT, RIGHT)
218
 
 
219
 
    def __init__(self, axis_index, axis_type, **kw):
220
 
        assert(axis_type in Axis.TYPES)
221
 
        self.has_style = False
222
 
        self.axis_index = axis_index
223
 
        self.axis_type = axis_type
224
 
        self.positions = None
225
 
 
226
 
    def set_index(self, axis_index):
227
 
        self.axis_index = axis_index
228
 
 
229
 
    def set_positions(self, positions):
230
 
        self.positions = positions
231
 
 
232
 
    def set_style(self, colour, font_size=None, alignment=None):
233
 
        _check_colour(colour)
234
 
        self.colour = colour
235
 
        self.font_size = font_size
236
 
        self.alignment = alignment
237
 
        self.has_style = True
238
 
 
239
 
    def style_to_url(self):
240
 
        bits = []
241
 
        bits.append(str(self.axis_index))
242
 
        bits.append(self.colour)
243
 
        if self.font_size is not None:
244
 
            bits.append(str(self.font_size))
245
 
            if self.alignment is not None:
246
 
                bits.append(str(self.alignment))
247
 
        return ','.join(bits)
248
 
 
249
 
    def positions_to_url(self):
250
 
        bits = []
251
 
        bits.append(str(self.axis_index))
252
 
        bits += [str(a) for a in self.positions]
253
 
        return ','.join(bits)
254
 
 
255
 
 
256
 
class LabelAxis(Axis):
257
 
 
258
 
    def __init__(self, axis_index, axis_type, values, **kwargs):
259
 
        Axis.__init__(self, axis_index, axis_type, **kwargs)
260
 
        self.values = [str(a) for a in values]
261
 
 
262
 
    def __repr__(self):
263
 
        return '%i:%%7c%s' % (self.axis_index, '%7c'.join(self.values))
264
 
 
265
 
 
266
 
class RangeAxis(Axis):
267
 
 
268
 
    def __init__(self, axis_index, axis_type, low, high, **kwargs):
269
 
        Axis.__init__(self, axis_index, axis_type, **kwargs)
270
 
        self.low = low
271
 
        self.high = high
272
 
 
273
 
    def __repr__(self):
274
 
        return '%i,%s,%s' % (self.axis_index, self.low, self.high)
275
 
 
276
 
# Chart Classes
277
 
# -----------------------------------------------------------------------------
278
 
 
279
 
 
280
 
class Chart(object):
281
 
    """Abstract class for all chart types.
282
 
 
283
 
    width are height specify the dimensions of the image. title sets the title
284
 
    of the chart. legend requires a list that corresponds to datasets.
285
 
    """
286
 
 
287
 
    BASE_URL = 'http://chart.apis.google.com/chart?'
288
 
    BACKGROUND = 'bg'
289
 
    CHART = 'c'
290
 
    ALPHA = 'a'
291
 
    VALID_SOLID_FILL_TYPES = (BACKGROUND, CHART, ALPHA)
292
 
    SOLID = 's'
293
 
    LINEAR_GRADIENT = 'lg'
294
 
    LINEAR_STRIPES = 'ls'
295
 
 
296
 
    def __init__(self, width, height, title=None, legend=None, colours=None,
297
 
            auto_scale=True, x_range=None, y_range=None,
298
 
            colours_within_series=None):
299
 
        if type(self) == Chart:
300
 
            raise AbstractClassException('This is an abstract class')
301
 
        assert(isinstance(width, int))
302
 
        assert(isinstance(height, int))
303
 
        self.width = width
304
 
        self.height = height
305
 
        self.data = []
306
 
        self.set_title(title)
307
 
        self.set_title_style(None, None)
308
 
        self.set_legend(legend)
309
 
        self.set_legend_position(None)
310
 
        self.set_colours(colours)
311
 
        self.set_colours_within_series(colours_within_series)
312
 
 
313
 
        # Data for scaling.
314
 
        self.auto_scale = auto_scale  # Whether to automatically scale data
315
 
        self.x_range = x_range  # (min, max) x-axis range for scaling
316
 
        self.y_range = y_range  # (min, max) y-axis range for scaling
317
 
        self.scaled_data_class = None
318
 
        self.scaled_x_range = None
319
 
        self.scaled_y_range = None
320
 
 
321
 
        self.fill_types = {
322
 
            Chart.BACKGROUND: None,
323
 
            Chart.CHART: None,
324
 
            Chart.ALPHA: None,
325
 
        }
326
 
        self.fill_area = {
327
 
            Chart.BACKGROUND: None,
328
 
            Chart.CHART: None,
329
 
            Chart.ALPHA: None,
330
 
        }
331
 
        self.axis = []
332
 
        self.markers = []
333
 
        self.line_styles = {}
334
 
        self.grid = None
335
 
 
336
 
    # URL generation
337
 
    # -------------------------------------------------------------------------
338
 
 
339
 
    def get_url(self, data_class=None):
340
 
        url_bits = self.get_url_bits(data_class=data_class)
341
 
        return self.BASE_URL + '&'.join(url_bits)
342
 
 
343
 
    def get_url_bits(self, data_class=None):
344
 
        url_bits = []
345
 
        # required arguments
346
 
        url_bits.append(self.type_to_url())
347
 
        url_bits.append('chs=%ix%i' % (self.width, self.height))
348
 
        url_bits.append(self.data_to_url(data_class=data_class))
349
 
        # optional arguments
350
 
        if self.title:
351
 
            url_bits.append('chtt=%s' % self.title)
352
 
        if self.title_colour and self.title_font_size:
353
 
            url_bits.append('chts=%s,%s' % (self.title_colour, \
354
 
                self.title_font_size))
355
 
        if self.legend:
356
 
            url_bits.append('chdl=%s' % '%7c'.join(self.legend))
357
 
        if self.legend_position:
358
 
            url_bits.append('chdlp=%s' % (self.legend_position))
359
 
        if self.colours:
360
 
            url_bits.append('chco=%s' % ','.join(self.colours))            
361
 
        if self.colours_within_series:
362
 
            url_bits.append('chco=%s' % '%7c'.join(self.colours_within_series))
363
 
        ret = self.fill_to_url()
364
 
        if ret:
365
 
            url_bits.append(ret)
366
 
        ret = self.axis_to_url()
367
 
        if ret:
368
 
            url_bits.append(ret)                    
369
 
        if self.markers:
370
 
            url_bits.append(self.markers_to_url())        
371
 
        if self.line_styles:
372
 
            style = []
373
 
            for index in xrange(max(self.line_styles) + 1):
374
 
                if index in self.line_styles:
375
 
                    values = self.line_styles[index]
376
 
                else:
377
 
                    values = ('1', )
378
 
                style.append(','.join(values))
379
 
            url_bits.append('chls=%s' % '%7c'.join(style))
380
 
        if self.grid:
381
 
            url_bits.append('chg=%s' % self.grid)
382
 
        return url_bits
383
 
 
384
 
    # Downloading
385
 
    # -------------------------------------------------------------------------
386
 
 
387
 
    def download(self, file_name):
388
 
        opener = urllib2.urlopen(self.get_url())
389
 
 
390
 
        if opener.headers['content-type'] != 'image/png':
391
 
            raise BadContentTypeException('Server responded with a ' \
392
 
                'content-type of %s' % opener.headers['content-type'])
393
 
 
394
 
        open(file_name, 'wb').write(opener.read())
395
 
 
396
 
    # Simple settings
397
 
    # -------------------------------------------------------------------------
398
 
 
399
 
    def set_title(self, title):
400
 
        if title:
401
 
            self.title = urllib.quote(title)
402
 
        else:
403
 
            self.title = None
404
 
 
405
 
    def set_title_style(self, colour, font_size):
406
 
        if not colour is None:
407
 
            _check_colour(colour)
408
 
        self.title_colour = colour
409
 
        self.title_font_size = font_size
410
 
 
411
 
    def set_legend(self, legend):
412
 
        """legend needs to be a list, tuple or None"""
413
 
        assert(isinstance(legend, list) or isinstance(legend, tuple) or
414
 
            legend is None)
415
 
        if legend:
416
 
            self.legend = [urllib.quote(a) for a in legend]
417
 
        else:
418
 
            self.legend = None
419
 
 
420
 
    def set_legend_position(self, legend_position):
421
 
        if legend_position:
422
 
            self.legend_position = urllib.quote(legend_position)
423
 
        else:    
424
 
            self.legend_position = None
425
 
 
426
 
    # Chart colours
427
 
    # -------------------------------------------------------------------------
428
 
 
429
 
    def set_colours(self, colours):
430
 
        # colours needs to be a list, tuple or None
431
 
        assert(isinstance(colours, list) or isinstance(colours, tuple) or
432
 
            colours is None)
433
 
        # make sure the colours are in the right format
434
 
        if colours:
435
 
            for col in colours:
436
 
                _check_colour(col)
437
 
        self.colours = colours
438
 
 
439
 
    def set_colours_within_series(self, colours):
440
 
        # colours needs to be a list, tuple or None
441
 
        assert(isinstance(colours, list) or isinstance(colours, tuple) or
442
 
            colours is None)
443
 
        # make sure the colours are in the right format
444
 
        if colours:
445
 
            for col in colours:
446
 
                _check_colour(col)
447
 
        self.colours_within_series = colours        
448
 
 
449
 
    # Background/Chart colours
450
 
    # -------------------------------------------------------------------------
451
 
 
452
 
    def fill_solid(self, area, colour):
453
 
        assert(area in Chart.VALID_SOLID_FILL_TYPES)
454
 
        _check_colour(colour)
455
 
        self.fill_area[area] = colour
456
 
        self.fill_types[area] = Chart.SOLID
457
 
 
458
 
    def _check_fill_linear(self, angle, *args):
459
 
        assert(isinstance(args, list) or isinstance(args, tuple))
460
 
        assert(angle >= 0 and angle <= 90)
461
 
        assert(len(args) % 2 == 0)
462
 
        args = list(args)  # args is probably a tuple and we need to mutate
463
 
        for a in xrange(int(len(args) / 2)):
464
 
            col = args[a * 2]
465
 
            offset = args[a * 2 + 1]
466
 
            _check_colour(col)
467
 
            assert(offset >= 0 and offset <= 1)
468
 
            args[a * 2 + 1] = str(args[a * 2 + 1])
469
 
        return args
470
 
 
471
 
    def fill_linear_gradient(self, area, angle, *args):
472
 
        assert(area in Chart.VALID_SOLID_FILL_TYPES)
473
 
        args = self._check_fill_linear(angle, *args)
474
 
        self.fill_types[area] = Chart.LINEAR_GRADIENT
475
 
        self.fill_area[area] = ','.join([str(angle)] + args)
476
 
 
477
 
    def fill_linear_stripes(self, area, angle, *args):
478
 
        assert(area in Chart.VALID_SOLID_FILL_TYPES)
479
 
        args = self._check_fill_linear(angle, *args)
480
 
        self.fill_types[area] = Chart.LINEAR_STRIPES
481
 
        self.fill_area[area] = ','.join([str(angle)] + args)
482
 
 
483
 
    def fill_to_url(self):
484
 
        areas = []
485
 
        for area in (Chart.BACKGROUND, Chart.CHART, Chart.ALPHA):
486
 
            if self.fill_types[area]:
487
 
                areas.append('%s,%s,%s' % (area, self.fill_types[area], \
488
 
                    self.fill_area[area]))
489
 
        if areas:
490
 
            return 'chf=' + '%7c'.join(areas)
491
 
 
492
 
    # Data
493
 
    # -------------------------------------------------------------------------
494
 
 
495
 
    def data_class_detection(self, data):
496
 
        """Determines the appropriate data encoding type to give satisfactory
497
 
        resolution (http://code.google.com/apis/chart/#chart_data).
498
 
        """
499
 
        assert(isinstance(data, list) or isinstance(data, tuple))
500
 
        if not isinstance(self, (LineChart, BarChart, ScatterChart)):
501
 
            # From the link above:
502
 
            #   Simple encoding is suitable for all other types of chart
503
 
            #   regardless of size.
504
 
            return SimpleData
505
 
        elif self.height < 100:
506
 
            # The link above indicates that line and bar charts less
507
 
            # than 300px in size can be suitably represented with the
508
 
            # simple encoding. I've found that this isn't sufficient,
509
 
            # e.g. examples/line-xy-circle.png. Let's try 100px.
510
 
            return SimpleData
511
 
        else:
512
 
            return ExtendedData
513
 
 
514
 
    def _filter_none(self, data):
515
 
        return [r for r in data if r is not None]
516
 
 
517
 
    def data_x_range(self):
518
 
        """Return a 2-tuple giving the minimum and maximum x-axis
519
 
        data range.
520
 
        """
521
 
        try:
522
 
            lower = min([min(self._filter_none(s))
523
 
                         for type, s in self.annotated_data()
524
 
                         if type == 'x'])
525
 
            upper = max([max(self._filter_none(s))
526
 
                         for type, s in self.annotated_data()
527
 
                         if type == 'x'])
528
 
            return (lower, upper)
529
 
        except ValueError:
530
 
            return None     # no x-axis datasets
531
 
 
532
 
    def data_y_range(self):
533
 
        """Return a 2-tuple giving the minimum and maximum y-axis
534
 
        data range.
535
 
        """
536
 
        try:
537
 
            lower = min([min(self._filter_none(s))
538
 
                         for type, s in self.annotated_data()
539
 
                         if type == 'y'])
540
 
            upper = max([max(self._filter_none(s)) + 1
541
 
                         for type, s in self.annotated_data()
542
 
                         if type == 'y'])
543
 
            return (lower, upper)
544
 
        except ValueError:
545
 
            return None     # no y-axis datasets
546
 
 
547
 
    def scaled_data(self, data_class, x_range=None, y_range=None):
548
 
        """Scale `self.data` as appropriate for the given data encoding
549
 
        (data_class) and return it.
550
 
 
551
 
        An optional `y_range` -- a 2-tuple (lower, upper) -- can be
552
 
        given to specify the y-axis bounds. If not given, the range is
553
 
        inferred from the data: (0, <max-value>) presuming no negative
554
 
        values, or (<min-value>, <max-value>) if there are negative
555
 
        values.  `self.scaled_y_range` is set to the actual lower and
556
 
        upper scaling range.
557
 
 
558
 
        Ditto for `x_range`. Note that some chart types don't have x-axis
559
 
        data.
560
 
        """
561
 
        self.scaled_data_class = data_class
562
 
 
563
 
        # Determine the x-axis range for scaling.
564
 
        if x_range is None:
565
 
            x_range = self.data_x_range()
566
 
            if x_range and x_range[0] > 0:
567
 
                x_range = (x_range[0], x_range[1])
568
 
        self.scaled_x_range = x_range
569
 
 
570
 
        # Determine the y-axis range for scaling.
571
 
        if y_range is None:
572
 
            y_range = self.data_y_range()
573
 
            if y_range and y_range[0] > 0:
574
 
                y_range = (y_range[0], y_range[1])
575
 
        self.scaled_y_range = y_range
576
 
 
577
 
        scaled_data = []
578
 
        for type, dataset in self.annotated_data():
579
 
            if type == 'x':
580
 
                scale_range = x_range
581
 
            elif type == 'y':
582
 
                scale_range = y_range
583
 
            elif type == 'marker-size':
584
 
                scale_range = (0, max(dataset))
585
 
            scaled_dataset = []
586
 
            for v in dataset:
587
 
                if v is None:
588
 
                    scaled_dataset.append(None)
589
 
                else:
590
 
                    scaled_dataset.append(
591
 
                        data_class.scale_value(v, scale_range))
592
 
            scaled_data.append(scaled_dataset)
593
 
        return scaled_data
594
 
 
595
 
    def add_data(self, data):
596
 
        self.data.append(data)
597
 
        return len(self.data) - 1  # return the "index" of the data set
598
 
 
599
 
    def data_to_url(self, data_class=None):
600
 
        if not data_class:
601
 
            data_class = self.data_class_detection(self.data)
602
 
        if not issubclass(data_class, Data):
603
 
            raise UnknownDataTypeException()
604
 
        if self.auto_scale:
605
 
            data = self.scaled_data(data_class, self.x_range, self.y_range)
606
 
        else:
607
 
            data = self.data
608
 
        return repr(data_class(data))
609
 
 
610
 
    def annotated_data(self):
611
 
        for dataset in self.data:
612
 
            yield ('x', dataset)
613
 
 
614
 
    # Axis Labels
615
 
    # -------------------------------------------------------------------------
616
 
 
617
 
    def set_axis_labels(self, axis_type, values):
618
 
        assert(axis_type in Axis.TYPES)
619
 
        values = [urllib.quote(str(a)) for a in values]
620
 
        axis_index = len(self.axis)
621
 
        axis = LabelAxis(axis_index, axis_type, values)
622
 
        self.axis.append(axis)
623
 
        return axis_index
624
 
 
625
 
    def set_axis_range(self, axis_type, low, high):
626
 
        assert(axis_type in Axis.TYPES)
627
 
        axis_index = len(self.axis)
628
 
        axis = RangeAxis(axis_index, axis_type, low, high)
629
 
        self.axis.append(axis)
630
 
        return axis_index
631
 
 
632
 
    def set_axis_positions(self, axis_index, positions):
633
 
        try:
634
 
            self.axis[axis_index].set_positions(positions)
635
 
        except IndexError:
636
 
            raise InvalidParametersException('Axis index %i has not been ' \
637
 
                'created' % axis)
638
 
 
639
 
    def set_axis_style(self, axis_index, colour, font_size=None, \
640
 
            alignment=None):
641
 
        try:
642
 
            self.axis[axis_index].set_style(colour, font_size, alignment)
643
 
        except IndexError:
644
 
            raise InvalidParametersException('Axis index %i has not been ' \
645
 
                'created' % axis)
646
 
 
647
 
    def axis_to_url(self):
648
 
        available_axis = []
649
 
        label_axis = []
650
 
        range_axis = []
651
 
        positions = []
652
 
        styles = []
653
 
        index = -1
654
 
        for axis in self.axis:
655
 
            available_axis.append(axis.axis_type)
656
 
            if isinstance(axis, RangeAxis):
657
 
                range_axis.append(repr(axis))
658
 
            if isinstance(axis, LabelAxis):
659
 
                label_axis.append(repr(axis))
660
 
            if axis.positions:
661
 
                positions.append(axis.positions_to_url())
662
 
            if axis.has_style:
663
 
                styles.append(axis.style_to_url())
664
 
        if not available_axis:
665
 
            return
666
 
        url_bits = []
667
 
        url_bits.append('chxt=%s' % ','.join(available_axis))
668
 
        if label_axis:
669
 
            url_bits.append('chxl=%s' % '%7c'.join(label_axis))
670
 
        if range_axis:
671
 
            url_bits.append('chxr=%s' % '%7c'.join(range_axis))
672
 
        if positions:
673
 
            url_bits.append('chxp=%s' % '%7c'.join(positions))
674
 
        if styles:
675
 
            url_bits.append('chxs=%s' % '%7c'.join(styles))
676
 
        return '&'.join(url_bits)
677
 
 
678
 
    # Markers, Ranges and Fill area (chm)
679
 
    # -------------------------------------------------------------------------
680
 
 
681
 
    def markers_to_url(self):        
682
 
        return 'chm=%s' % '%7c'.join([','.join(a) for a in self.markers])
683
 
 
684
 
    def add_marker(self, index, point, marker_type, colour, size, priority=0):
685
 
        self.markers.append((marker_type, colour, str(index), str(point), \
686
 
            str(size), str(priority)))
687
 
 
688
 
    def add_horizontal_range(self, colour, start, stop):
689
 
        self.markers.append(('r', colour, '0', str(start), str(stop)))
690
 
 
691
 
    def add_data_line(self, colour, data_set, size, priority=0):
692
 
        self.markers.append(('D', colour, str(data_set), '0', str(size), \
693
 
            str(priority)))
694
 
 
695
 
    def add_marker_text(self, string, colour, data_set, data_point, size, \
696
 
            priority=0):
697
 
        self.markers.append((str(string), colour, str(data_set), \
698
 
            str(data_point), str(size), str(priority)))        
699
 
 
700
 
    def add_vertical_range(self, colour, start, stop):
701
 
        self.markers.append(('R', colour, '0', str(start), str(stop)))
702
 
 
703
 
    def add_fill_range(self, colour, index_start, index_end):
704
 
        self.markers.append(('b', colour, str(index_start), str(index_end), \
705
 
            '1'))
706
 
 
707
 
    def add_fill_simple(self, colour):
708
 
        self.markers.append(('B', colour, '1', '1', '1'))
709
 
 
710
 
    # Line styles
711
 
    # -------------------------------------------------------------------------
712
 
 
713
 
    def set_line_style(self, index, thickness=1, line_segment=None, \
714
 
            blank_segment=None):
715
 
        value = []
716
 
        value.append(str(thickness))
717
 
        if line_segment:
718
 
            value.append(str(line_segment))
719
 
            value.append(str(blank_segment))
720
 
        self.line_styles[index] = value
721
 
 
722
 
    # Grid
723
 
    # -------------------------------------------------------------------------
724
 
 
725
 
    def set_grid(self, x_step, y_step, line_segment=1, \
726
 
            blank_segment=0):
727
 
        self.grid = '%s,%s,%s,%s' % (x_step, y_step, line_segment, \
728
 
            blank_segment)
729
 
 
730
 
 
731
 
class ScatterChart(Chart):
732
 
 
733
 
    def type_to_url(self):
734
 
        return 'cht=s'
735
 
 
736
 
    def annotated_data(self):
737
 
        yield ('x', self.data[0])
738
 
        yield ('y', self.data[1])
739
 
        if len(self.data) > 2:
740
 
            # The optional third dataset is relative sizing for point
741
 
            # markers.
742
 
            yield ('marker-size', self.data[2])
743
 
 
744
 
 
745
 
class LineChart(Chart):
746
 
 
747
 
    def __init__(self, *args, **kwargs):
748
 
        if type(self) == LineChart:
749
 
            raise AbstractClassException('This is an abstract class')
750
 
        Chart.__init__(self, *args, **kwargs)
751
 
 
752
 
 
753
 
class SimpleLineChart(LineChart):
754
 
 
755
 
    def type_to_url(self):
756
 
        return 'cht=lc'
757
 
 
758
 
    def annotated_data(self):
759
 
        # All datasets are y-axis data.
760
 
        for dataset in self.data:
761
 
            yield ('y', dataset)
762
 
 
763
 
 
764
 
class SparkLineChart(SimpleLineChart):
765
 
 
766
 
    def type_to_url(self):
767
 
        return 'cht=ls'
768
 
 
769
 
 
770
 
class XYLineChart(LineChart):
771
 
 
772
 
    def type_to_url(self):
773
 
        return 'cht=lxy'
774
 
 
775
 
    def annotated_data(self):
776
 
        # Datasets alternate between x-axis, y-axis.
777
 
        for i, dataset in enumerate(self.data):
778
 
            if i % 2 == 0:
779
 
                yield ('x', dataset)
780
 
            else:
781
 
                yield ('y', dataset)
782
 
 
783
 
 
784
 
class BarChart(Chart):
785
 
 
786
 
    def __init__(self, *args, **kwargs):
787
 
        if type(self) == BarChart:
788
 
            raise AbstractClassException('This is an abstract class')
789
 
        Chart.__init__(self, *args, **kwargs)
790
 
        self.bar_width = None
791
 
        self.zero_lines = {}
792
 
 
793
 
    def set_bar_width(self, bar_width):
794
 
        self.bar_width = bar_width
795
 
 
796
 
    def set_zero_line(self, index, zero_line):
797
 
        self.zero_lines[index] = zero_line
798
 
 
799
 
    def get_url_bits(self, data_class=None, skip_chbh=False):
800
 
        url_bits = Chart.get_url_bits(self, data_class=data_class)
801
 
        if not skip_chbh and self.bar_width is not None:
802
 
            url_bits.append('chbh=%i' % self.bar_width)
803
 
        zero_line = []
804
 
        if self.zero_lines:
805
 
            for index in xrange(max(self.zero_lines) + 1):
806
 
                if index in self.zero_lines:
807
 
                    zero_line.append(str(self.zero_lines[index]))
808
 
                else:
809
 
                    zero_line.append('0')
810
 
            url_bits.append('chp=%s' % ','.join(zero_line))
811
 
        return url_bits
812
 
 
813
 
 
814
 
class StackedHorizontalBarChart(BarChart):
815
 
 
816
 
    def type_to_url(self):
817
 
        return 'cht=bhs'
818
 
 
819
 
 
820
 
class StackedVerticalBarChart(BarChart):
821
 
 
822
 
    def type_to_url(self):
823
 
        return 'cht=bvs'
824
 
 
825
 
    def annotated_data(self):
826
 
        for dataset in self.data:
827
 
            yield ('y', dataset)
828
 
 
829
 
 
830
 
class GroupedBarChart(BarChart):
831
 
 
832
 
    def __init__(self, *args, **kwargs):
833
 
        if type(self) == GroupedBarChart:
834
 
            raise AbstractClassException('This is an abstract class')
835
 
        BarChart.__init__(self, *args, **kwargs)
836
 
        self.bar_spacing = None
837
 
        self.group_spacing = None
838
 
 
839
 
    def set_bar_spacing(self, spacing):
840
 
        """Set spacing between bars in a group."""
841
 
        self.bar_spacing = spacing
842
 
 
843
 
    def set_group_spacing(self, spacing):
844
 
        """Set spacing between groups of bars."""
845
 
        self.group_spacing = spacing
846
 
 
847
 
    def get_url_bits(self, data_class=None):
848
 
        # Skip 'BarChart.get_url_bits' and call Chart directly so the parent
849
 
        # doesn't add "chbh" before we do.
850
 
        url_bits = BarChart.get_url_bits(self, data_class=data_class,
851
 
            skip_chbh=True)
852
 
        if self.group_spacing is not None:
853
 
            if self.bar_spacing is None:
854
 
                raise InvalidParametersException('Bar spacing is required ' \
855
 
                    'to be set when setting group spacing')
856
 
            if self.bar_width is None:
857
 
                raise InvalidParametersException('Bar width is required to ' \
858
 
                    'be set when setting bar spacing')
859
 
            url_bits.append('chbh=%i,%i,%i'
860
 
                % (self.bar_width, self.bar_spacing, self.group_spacing))
861
 
        elif self.bar_spacing is not None:
862
 
            if self.bar_width is None:
863
 
                raise InvalidParametersException('Bar width is required to ' \
864
 
                    'be set when setting bar spacing')
865
 
            url_bits.append('chbh=%i,%i' % (self.bar_width, self.bar_spacing))
866
 
        elif self.bar_width:
867
 
            url_bits.append('chbh=%i' % self.bar_width)
868
 
        return url_bits
869
 
 
870
 
 
871
 
class GroupedHorizontalBarChart(GroupedBarChart):
872
 
 
873
 
    def type_to_url(self):
874
 
        return 'cht=bhg'
875
 
 
876
 
 
877
 
class GroupedVerticalBarChart(GroupedBarChart):
878
 
 
879
 
    def type_to_url(self):
880
 
        return 'cht=bvg'
881
 
 
882
 
    def annotated_data(self):
883
 
        for dataset in self.data:
884
 
            yield ('y', dataset)
885
 
 
886
 
 
887
 
class PieChart(Chart):
888
 
 
889
 
    def __init__(self, *args, **kwargs):
890
 
        if type(self) == PieChart:
891
 
            raise AbstractClassException('This is an abstract class')
892
 
        Chart.__init__(self, *args, **kwargs)
893
 
        self.pie_labels = []
894
 
        if self.y_range:
895
 
            warnings.warn('y_range is not used with %s.' % \
896
 
                (self.__class__.__name__))
897
 
 
898
 
    def set_pie_labels(self, labels):
899
 
        self.pie_labels = [urllib.quote(a) for a in labels]
900
 
 
901
 
    def get_url_bits(self, data_class=None):
902
 
        url_bits = Chart.get_url_bits(self, data_class=data_class)
903
 
        if self.pie_labels:
904
 
            url_bits.append('chl=%s' % '%7c'.join(self.pie_labels))
905
 
        return url_bits
906
 
 
907
 
    def annotated_data(self):
908
 
        # Datasets are all y-axis data. However, there should only be
909
 
        # one dataset for pie charts.
910
 
        for dataset in self.data:
911
 
            yield ('x', dataset)
912
 
 
913
 
    def scaled_data(self, data_class, x_range=None, y_range=None):
914
 
        if not x_range:
915
 
            x_range = [0, sum(self.data[0])]
916
 
        return Chart.scaled_data(self, data_class, x_range, self.y_range)
917
 
 
918
 
 
919
 
class PieChart2D(PieChart):
920
 
 
921
 
    def type_to_url(self):
922
 
        return 'cht=p'
923
 
 
924
 
 
925
 
class PieChart3D(PieChart):
926
 
 
927
 
    def type_to_url(self):
928
 
        return 'cht=p3'
929
 
 
930
 
 
931
 
class VennChart(Chart):
932
 
 
933
 
    def type_to_url(self):
934
 
        return 'cht=v'
935
 
 
936
 
    def annotated_data(self):
937
 
        for dataset in self.data:
938
 
            yield ('y', dataset)
939
 
 
940
 
 
941
 
class RadarChart(Chart):
942
 
 
943
 
    def type_to_url(self):
944
 
        return 'cht=r'
945
 
 
946
 
 
947
 
class SplineRadarChart(RadarChart):
948
 
 
949
 
    def type_to_url(self):
950
 
        return 'cht=rs'
951
 
 
952
 
 
953
 
class MapChart(Chart):
954
 
 
955
 
    def __init__(self, *args, **kwargs):
956
 
        Chart.__init__(self, *args, **kwargs)
957
 
        self.geo_area = 'world'
958
 
        self.codes = []
959
 
        self.__areas = ('africa', 'asia', 'europe', 'middle_east', 'south_america', 'usa', 'world')
960
 
        self.__ccodes = ('AD', 'AE', 'AF', 'AG', 'AI', 'AL', 'AM', 'AN', 'AO', 'AQ', 'AR',
961
 
                         'AS', 'AT', 'AU', 'AW', 'AX', 'AZ', 'BA', 'BB', 'BD', 'BE', 'BF',
962
 
                         'BG', 'BH', 'BI', 'BJ', 'BL', 'BM', 'BN', 'BO', 'BR', 'BS', 'BT',
963
 
                         'BV', 'BW', 'BY', 'BZ', 'CA', 'CC', 'CD', 'CF', 'CG', 'CH', 'CI',
964
 
                         'CK', 'CL', 'CM', 'CN', 'CO', 'CR', 'CU', 'CV', 'CX', 'CY', 'CZ',
965
 
                         'DE', 'DJ', 'DK', 'DM', 'DO', 'DZ', 'EC', 'EE', 'EG', 'EH', 'ER',
966
 
                         'ES', 'ET', 'FI', 'FJ', 'FK', 'FM', 'FO', 'FR', 'GA', 'GB', 'GD',
967
 
                         'GE', 'GF', 'GG', 'GH', 'GI', 'GL', 'GM', 'GN', 'GP', 'GQ', 'GR',
968
 
                         'GS', 'GT', 'GU', 'GW', 'GY', 'HK', 'HM', 'HN', 'HR', 'HT', 'HU',
969
 
                         'ID', 'IE', 'IL', 'IM', 'IN', 'IO', 'IQ', 'IR', 'IS', 'IT', 'JE',
970
 
                         'JM', 'JO', 'JP', 'KE', 'KG', 'KH', 'KI', 'KM', 'KN', 'KP', 'KR',
971
 
                         'KW', 'KY', 'KZ', 'LA', 'LB', 'LC', 'LI', 'LK', 'LR', 'LS', 'LT',
972
 
                         'LU', 'LV', 'LY', 'MA', 'MC', 'MD', 'ME', 'MF', 'MG', 'MH', 'MK',
973
 
                         'ML', 'MM', 'MN', 'MO', 'MP', 'MQ', 'MR', 'MS', 'MT', 'MU', 'MV',
974
 
                         'MW', 'MX', 'MY', 'MZ', 'NA', 'NC', 'NE', 'NF', 'NG', 'NI', 'NL',
975
 
                         'NO', 'NP', 'NR', 'NU', 'NZ', 'OM', 'PA', 'PE', 'PF', 'PG', 'PH',
976
 
                         'PK', 'PL', 'PM', 'PN', 'PR', 'PS', 'PT', 'PW', 'PY', 'QA', 'RE',
977
 
                         'RO', 'RS', 'RU', 'RW', 'SA', 'SB', 'SC', 'SD', 'SE', 'SG', 'SH',
978
 
                         'SI', 'SJ', 'SK', 'SL', 'SM', 'SN', 'SO', 'SR', 'ST', 'SV', 'SY',
979
 
                         'SZ', 'TC', 'TD', 'TF', 'TG', 'TH', 'TJ', 'TK', 'TL', 'TM', 'TN',
980
 
                         'TO', 'TR', 'TT', 'TV', 'TW', 'TZ', 'UA', 'UG', 'UM', 'US', 'UY',
981
 
                         'UZ', 'VA', 'VC', 'VE', 'VG', 'VI', 'VN', 'VU', 'WF', 'WS', 'YE',
982
 
                         'YT', 'ZA', 'ZM', 'ZW')
983
 
        
984
 
    def type_to_url(self):
985
 
        return 'cht=t'
986
 
 
987
 
    def set_codes(self, codes):
988
 
        '''Set the country code map for the data.
989
 
        Codes given in a list.
990
 
 
991
 
        i.e. DE - Germany
992
 
             AT - Austria
993
 
             US - United States
994
 
        '''
995
 
 
996
 
        codemap = ''
997
 
        
998
 
        for cc in codes:
999
 
            cc = cc.upper()
1000
 
            if cc in self.__ccodes:
1001
 
                codemap += cc
1002
 
            else:
1003
 
                raise UnknownCountryCodeException(cc)
1004
 
            
1005
 
        self.codes = codemap
1006
 
 
1007
 
    def set_geo_area(self, area):
1008
 
        '''Sets the geo area for the map.
1009
 
 
1010
 
        * africa
1011
 
        * asia
1012
 
        * europe
1013
 
        * middle_east
1014
 
        * south_america
1015
 
        * usa
1016
 
        * world
1017
 
        '''
1018
 
        
1019
 
        if area in self.__areas:
1020
 
            self.geo_area = area
1021
 
        else:
1022
 
            raise UnknownChartType('Unknown chart type for maps: %s' %area)
1023
 
 
1024
 
    def get_url_bits(self, data_class=None):
1025
 
        url_bits = Chart.get_url_bits(self, data_class=data_class)
1026
 
        url_bits.append('chtm=%s' % self.geo_area)
1027
 
        if self.codes:
1028
 
            url_bits.append('chld=%s' % ''.join(self.codes))
1029
 
        return url_bits
1030
 
 
1031
 
    def add_data_dict(self, datadict):
1032
 
        '''Sets the data and country codes via a dictionary.
1033
 
 
1034
 
        i.e. {'DE': 50, 'GB': 30, 'AT': 70}
1035
 
        '''
1036
 
 
1037
 
        self.set_codes(datadict.keys())
1038
 
        self.add_data(datadict.values())
1039
 
 
1040
 
 
1041
 
class GoogleOMeterChart(PieChart):
1042
 
    """Inheriting from PieChart because of similar labeling"""
1043
 
 
1044
 
    def __init__(self, *args, **kwargs):
1045
 
        PieChart.__init__(self, *args, **kwargs)
1046
 
        if self.auto_scale and not self.x_range:
1047
 
            warnings.warn('Please specify an x_range with GoogleOMeterChart, '
1048
 
                'otherwise one arrow will always be at the max.')
1049
 
 
1050
 
    def type_to_url(self):
1051
 
        return 'cht=gom'
1052
 
 
1053
 
 
1054
 
class QRChart(Chart):
1055
 
 
1056
 
    def __init__(self, *args, **kwargs):
1057
 
        Chart.__init__(self, *args, **kwargs)
1058
 
        self.encoding = None
1059
 
        self.ec_level = None
1060
 
        self.margin = None
1061
 
 
1062
 
    def type_to_url(self):
1063
 
        return 'cht=qr'
1064
 
 
1065
 
    def data_to_url(self, data_class=None):
1066
 
        if not self.data:
1067
 
            raise NoDataGivenException()
1068
 
        return 'chl=%s' % urllib.quote(self.data[0])
1069
 
 
1070
 
    def get_url_bits(self, data_class=None):
1071
 
        url_bits = Chart.get_url_bits(self, data_class=data_class)
1072
 
        if self.encoding:
1073
 
            url_bits.append('choe=%s' % self.encoding)
1074
 
        if self.ec_level:
1075
 
            url_bits.append('chld=%s%%7c%s' % (self.ec_level, self.margin))
1076
 
        return url_bits
1077
 
 
1078
 
    def set_encoding(self, encoding):
1079
 
        self.encoding = encoding
1080
 
 
1081
 
    def set_ec(self, level, margin):
1082
 
        self.ec_level = level
1083
 
        self.margin = margin
1084
 
 
1085
 
 
1086
 
class ChartGrammar(object):
1087
 
 
1088
 
    def __init__(self):
1089
 
        self.grammar = None
1090
 
        self.chart = None
1091
 
 
1092
 
    def parse(self, grammar):
1093
 
        self.grammar = grammar
1094
 
        self.chart = self.create_chart_instance()
1095
 
 
1096
 
        for attr in self.grammar:
1097
 
            if attr in ('w', 'h', 'type', 'auto_scale', 'x_range', 'y_range'):
1098
 
                continue  # These are already parsed in create_chart_instance
1099
 
            attr_func = 'parse_' + attr
1100
 
            if not hasattr(self, attr_func):
1101
 
                warnings.warn('No parser for grammar attribute "%s"' % (attr))
1102
 
                continue
1103
 
            getattr(self, attr_func)(grammar[attr])
1104
 
 
1105
 
        return self.chart
1106
 
 
1107
 
    def parse_data(self, data):
1108
 
        self.chart.data = data
1109
 
 
1110
 
    @staticmethod
1111
 
    def get_possible_chart_types():
1112
 
        possible_charts = []
1113
 
        for cls_name in globals().keys():
1114
 
            if not cls_name.endswith('Chart'):
1115
 
                continue
1116
 
            cls = globals()[cls_name]
1117
 
            # Check if it is an abstract class
1118
 
            try:
1119
 
                a = cls(1, 1, auto_scale=False)
1120
 
                del a
1121
 
            except AbstractClassException:
1122
 
                continue
1123
 
            # Strip off "Class"
1124
 
            possible_charts.append(cls_name[:-5])
1125
 
        return possible_charts
1126
 
 
1127
 
    def create_chart_instance(self, grammar=None):
1128
 
        if not grammar:
1129
 
            grammar = self.grammar
1130
 
        assert(isinstance(grammar, dict))  # grammar must be a dict
1131
 
        assert('w' in grammar)  # width is required
1132
 
        assert('h' in grammar)  # height is required
1133
 
        assert('type' in grammar)  # type is required
1134
 
        chart_type = grammar['type']
1135
 
        w = grammar['w']
1136
 
        h = grammar['h']
1137
 
        auto_scale = grammar.get('auto_scale', None)
1138
 
        x_range = grammar.get('x_range', None)
1139
 
        y_range = grammar.get('y_range', None)
1140
 
        types = ChartGrammar.get_possible_chart_types()
1141
 
        if chart_type not in types:
1142
 
            raise UnknownChartType('%s is an unknown chart type. Possible '
1143
 
                'chart types are %s' % (chart_type, ','.join(types)))
1144
 
        return globals()[chart_type + 'Chart'](w, h, auto_scale=auto_scale,
1145
 
            x_range=x_range, y_range=y_range)
1146
 
 
1147
 
    def download(self):
1148
 
        pass
1149
 
 
 
1
"""
 
2
PyGoogleChart - A complete Python wrapper for the Google Chart API
 
3
 
 
4
http://pygooglechart.slowchop.com/
 
5
 
 
6
Copyright 2007 Gerald Kaszuba
 
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
 
29
 
 
30
# Helper variables and functions
 
31
# -----------------------------------------------------------------------------
 
32
 
 
33
__version__ = '0.1.2'
 
34
 
 
35
reo_colour = re.compile('^([A-Fa-f0-9]{2,2}){3,4}$')
 
36
 
 
37
 
 
38
def _check_colour(colour):
 
39
    if not reo_colour.match(colour):
 
40
        raise InvalidParametersException('Colours need to be in ' \
 
41
            'RRGGBB or RRGGBBAA format. One of your colours has %s' % \
 
42
            colour)
 
43
 
 
44
# Exception Classes
 
45
# -----------------------------------------------------------------------------
 
46
 
 
47
 
 
48
class PyGoogleChartException(Exception):
 
49
    pass
 
50
 
 
51
 
 
52
class DataOutOfRangeException(PyGoogleChartException):
 
53
    pass
 
54
 
 
55
 
 
56
class UnknownDataTypeException(PyGoogleChartException):
 
57
    pass
 
58
 
 
59
 
 
60
class NoDataGivenException(PyGoogleChartException):
 
61
    pass
 
62
 
 
63
 
 
64
class InvalidParametersException(PyGoogleChartException):
 
65
    pass
 
66
 
 
67
 
 
68
class BadContentTypeException(PyGoogleChartException):
 
69
    pass
 
70
 
 
71
 
 
72
# Data Classes
 
73
# -----------------------------------------------------------------------------
 
74
 
 
75
 
 
76
class Data(object):
 
77
 
 
78
    def __init__(self, data):
 
79
        assert(type(self) != Data)  # This is an abstract class
 
80
        self.data = data
 
81
 
 
82
 
 
83
class SimpleData(Data):
 
84
    enc_map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
 
85
 
 
86
    def __repr__(self):
 
87
        encoded_data = []
 
88
        for data in self.data:
 
89
            sub_data = []
 
90
            for value in data:
 
91
                if value is None:
 
92
                    sub_data.append('_')
 
93
                elif value >= 0 and value <= SimpleData.max_value:
 
94
                    sub_data.append(SimpleData.enc_map[value])
 
95
                else:
 
96
                    raise DataOutOfRangeException()
 
97
            encoded_data.append(''.join(sub_data))
 
98
        return 'chd=s:' + ','.join(encoded_data)
 
99
 
 
100
    @staticmethod
 
101
    def max_value():
 
102
        return 61
 
103
 
 
104
 
 
105
class TextData(Data):
 
106
 
 
107
    def __repr__(self):
 
108
        encoded_data = []
 
109
        for data in self.data:
 
110
            sub_data = []
 
111
            for value in data:
 
112
                if value is None:
 
113
                    sub_data.append(-1)
 
114
                elif value >= 0 and value <= TextData.max_value:
 
115
                    sub_data.append(str(float(value)))
 
116
                else:
 
117
                    raise DataOutOfRangeException()
 
118
            encoded_data.append(','.join(sub_data))
 
119
        return 'chd=t:' + '|'.join(encoded_data)
 
120
 
 
121
    @staticmethod
 
122
    def max_value():
 
123
        return 100
 
124
 
 
125
 
 
126
class ExtendedData(Data):
 
127
    enc_map = \
 
128
        'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-.'
 
129
 
 
130
    def __repr__(self):
 
131
        encoded_data = []
 
132
        enc_size = len(ExtendedData.enc_map)
 
133
        for data in self.data:
 
134
            sub_data = []
 
135
            for value in data:
 
136
                if value is None:
 
137
                    sub_data.append('__')
 
138
                elif value >= 0 and value <= ExtendedData.max_value:
 
139
                    first, second = divmod(int(value), enc_size)
 
140
                    sub_data.append('%s%s' % (
 
141
                        ExtendedData.enc_map[first],
 
142
                        ExtendedData.enc_map[second]))
 
143
                else:
 
144
                    raise DataOutOfRangeException( \
 
145
                        'Item #%i "%s" is out of range' % (data.index(value), \
 
146
                        value))
 
147
            encoded_data.append(''.join(sub_data))
 
148
        return 'chd=e:' + ','.join(encoded_data)
 
149
 
 
150
    @staticmethod
 
151
    def max_value():
 
152
        return 4095
 
153
 
 
154
# Axis Classes
 
155
# -----------------------------------------------------------------------------
 
156
 
 
157
 
 
158
class Axis(object):
 
159
    BOTTOM = 'x'
 
160
    TOP = 't'
 
161
    LEFT = 'y'
 
162
    RIGHT = 'r'
 
163
    TYPES = (BOTTOM, TOP, LEFT, RIGHT)
 
164
 
 
165
    def __init__(self, axis_index, axis_type, **kw):
 
166
        assert(axis_type in Axis.TYPES)
 
167
        self.has_style = False
 
168
        self.axis_index = axis_index
 
169
        self.axis_type = axis_type
 
170
        self.positions = None
 
171
 
 
172
    def set_index(self, axis_index):
 
173
        self.axis_index = axis_index
 
174
 
 
175
    def set_positions(self, positions):
 
176
        self.positions = positions
 
177
 
 
178
    def set_style(self, colour, font_size=None, alignment=None):
 
179
        _check_colour(colour)
 
180
        self.colour = colour
 
181
        self.font_size = font_size
 
182
        self.alignment = alignment
 
183
        self.has_style = True
 
184
 
 
185
    def style_to_url(self):
 
186
        bits = []
 
187
        bits.append(str(self.axis_index))
 
188
        bits.append(self.colour)
 
189
        if self.font_size is not None:
 
190
            bits.append(str(self.font_size))
 
191
            if self.alignment is not None:
 
192
                bits.append(str(self.alignment))
 
193
        return ','.join(bits)
 
194
 
 
195
    def positions_to_url(self):
 
196
        bits = []
 
197
        bits.append(str(self.axis_index))
 
198
        bits += [str(a) for a in self.positions]
 
199
        return ','.join(bits)
 
200
 
 
201
 
 
202
class LabelAxis(Axis):
 
203
 
 
204
    def __init__(self, axis_index, axis_type, values, **kwargs):
 
205
        Axis.__init__(self, axis_index, axis_type, **kwargs)
 
206
        self.values = [str(a) for a in values]
 
207
 
 
208
    def __repr__(self):
 
209
        return '%i:|%s' % (self.axis_index, '|'.join(self.values))
 
210
 
 
211
 
 
212
class RangeAxis(Axis):
 
213
 
 
214
    def __init__(self, axis_index, axis_type, low, high, **kwargs):
 
215
        Axis.__init__(self, axis_index, axis_type, **kwargs)
 
216
        self.low = low
 
217
        self.high = high
 
218
 
 
219
    def __repr__(self):
 
220
        return '%i,%s,%s' % (self.axis_index, self.low, self.high)
 
221
 
 
222
# Chart Classes
 
223
# -----------------------------------------------------------------------------
 
224
 
 
225
 
 
226
class Chart(object):
 
227
    """Abstract class for all chart types.
 
228
 
 
229
    width are height specify the dimensions of the image. title sets the title
 
230
    of the chart. legend requires a list that corresponds to datasets.
 
231
    """
 
232
 
 
233
    BASE_URL = 'http://chart.apis.google.com/chart?'
 
234
    BACKGROUND = 'bg'
 
235
    CHART = 'c'
 
236
    SOLID = 's'
 
237
    LINEAR_GRADIENT = 'lg'
 
238
    LINEAR_STRIPES = 'ls'
 
239
 
 
240
    def __init__(self, width, height, title=None, legend=None, colours=None):
 
241
        assert(type(self) != Chart)  # This is an abstract class
 
242
        assert(isinstance(width, int))
 
243
        assert(isinstance(height, int))
 
244
        self.width = width
 
245
        self.height = height
 
246
        self.data = []
 
247
        self.set_title(title)
 
248
        self.set_legend(legend)
 
249
        self.set_colours(colours)
 
250
        self.fill_types = {
 
251
            Chart.BACKGROUND: None,
 
252
            Chart.CHART: None,
 
253
        }
 
254
        self.fill_area = {
 
255
            Chart.BACKGROUND: None,
 
256
            Chart.CHART: None,
 
257
        }
 
258
#        self.axis = {
 
259
#            Axis.TOP: None,
 
260
#            Axis.BOTTOM: None,
 
261
#            Axis.LEFT: None,
 
262
#            Axis.RIGHT: None,
 
263
#        }
 
264
        self.axis = []
 
265
        self.markers = []
 
266
 
 
267
    # URL generation
 
268
    # -------------------------------------------------------------------------
 
269
 
 
270
    def get_url(self):
 
271
        url_bits = self.get_url_bits()
 
272
        return self.BASE_URL + '&'.join(url_bits)
 
273
 
 
274
    def get_url_bits(self):
 
275
        url_bits = []
 
276
        # required arguments
 
277
        url_bits.append(self.type_to_url())
 
278
        url_bits.append('chs=%ix%i' % (self.width, self.height))
 
279
        url_bits.append(self.data_to_url())
 
280
        # optional arguments
 
281
        if self.title:
 
282
            url_bits.append('chtt=%s' % self.title)
 
283
        if self.legend:
 
284
            url_bits.append('chdl=%s' % '|'.join(self.legend))
 
285
        if self.colours:
 
286
            url_bits.append('chco=%s' % ','.join(self.colours))
 
287
        ret = self.fill_to_url()
 
288
        if ret:
 
289
            url_bits.append(ret)
 
290
        ret = self.axis_to_url()
 
291
        if ret:
 
292
            url_bits.append(ret)
 
293
        if self.markers:
 
294
            url_bits.append(self.markers_to_url())
 
295
        return url_bits
 
296
 
 
297
    # Downloading
 
298
    # -------------------------------------------------------------------------
 
299
 
 
300
    def download(self, file_name):
 
301
        opener = urllib2.urlopen(self.get_url())
 
302
 
 
303
        if opener.headers['content-type'] != 'image/png':
 
304
            raise BadContentTypeException('Server responded with a ' \
 
305
                'content-type of %s' % opener.headers['content-type'])
 
306
 
 
307
        open(file_name, 'wb').write(urllib.urlopen(self.get_url()).read())
 
308
 
 
309
    # Simple settings
 
310
    # -------------------------------------------------------------------------
 
311
 
 
312
    def set_title(self, title):
 
313
        if title:
 
314
            self.title = urllib.quote(title)
 
315
        else:
 
316
            self.title = None
 
317
 
 
318
    def set_legend(self, legend):
 
319
        # legend needs to be a list, tuple or None
 
320
        assert(isinstance(legend, list) or isinstance(legend, tuple) or
 
321
            legend is None)
 
322
        if legend:
 
323
            self.legend = [urllib.quote(a) for a in legend]
 
324
        else:
 
325
            self.legend = None
 
326
 
 
327
    # Chart colours
 
328
    # -------------------------------------------------------------------------
 
329
 
 
330
    def set_colours(self, colours):
 
331
        # colours needs to be a list, tuple or None
 
332
        assert(isinstance(colours, list) or isinstance(colours, tuple) or
 
333
            colours is None)
 
334
        # make sure the colours are in the right format
 
335
        if colours:
 
336
            for col in colours:
 
337
                _check_colour(col)
 
338
        self.colours = colours
 
339
 
 
340
    # Background/Chart colours
 
341
    # -------------------------------------------------------------------------
 
342
 
 
343
    def fill_solid(self, area, colour):
 
344
        assert(area in (Chart.BACKGROUND, Chart.CHART))
 
345
        _check_colour(colour)
 
346
        self.fill_area[area] = colour
 
347
        self.fill_types[area] = Chart.SOLID
 
348
 
 
349
    def _check_fill_linear(self, angle, *args):
 
350
        assert(isinstance(args, list) or isinstance(args, tuple))
 
351
        assert(angle >= 0 and angle <= 90)
 
352
        assert(len(args) % 2 == 0)
 
353
        args = list(args)  # args is probably a tuple and we need to mutate
 
354
        for a in xrange(len(args) / 2):
 
355
            col = args[a * 2]
 
356
            offset = args[a * 2 + 1]
 
357
            _check_colour(col)
 
358
            assert(offset >= 0 and offset <= 1)
 
359
            args[a * 2 + 1] = str(args[a * 2 + 1])
 
360
        return args
 
361
 
 
362
    def fill_linear_gradient(self, area, angle, *args):
 
363
        assert(area in (Chart.BACKGROUND, Chart.CHART))
 
364
        args = self._check_fill_linear(angle, *args)
 
365
        self.fill_types[area] = Chart.LINEAR_GRADIENT
 
366
        self.fill_area[area] = ','.join([str(angle)] + args)
 
367
 
 
368
    def fill_linear_stripes(self, area, angle, *args):
 
369
        assert(area in (Chart.BACKGROUND, Chart.CHART))
 
370
        args = self._check_fill_linear(angle, *args)
 
371
        self.fill_types[area] = Chart.LINEAR_STRIPES
 
372
        self.fill_area[area] = ','.join([str(angle)] + args)
 
373
 
 
374
    def fill_to_url(self):
 
375
        areas = []
 
376
        for area in (Chart.BACKGROUND, Chart.CHART):
 
377
            if self.fill_types[area]:
 
378
                areas.append('%s,%s,%s' % (area, self.fill_types[area], \
 
379
                    self.fill_area[area]))
 
380
        if areas:
 
381
            return 'chf=' + '|'.join(areas)
 
382
 
 
383
    # Data
 
384
    # -------------------------------------------------------------------------
 
385
 
 
386
    def data_class_detection(self, data):
 
387
        """
 
388
        Detects and returns the data type required based on the range of the
 
389
        data given. The data given must be lists of numbers within a list.
 
390
        """
 
391
        assert(isinstance(data, list) or isinstance(data, tuple))
 
392
        max_value = None
 
393
        for a in data:
 
394
            assert(isinstance(a, list) or isinstance(a, tuple))
 
395
            if max_value is None or max(a) > max_value:
 
396
                max_value = max(a)
 
397
        for data_class in (SimpleData, TextData, ExtendedData):
 
398
            if max_value <= data_class.max_value():
 
399
                return data_class
 
400
        raise DataOutOfRangeException()
 
401
 
 
402
    def add_data(self, data):
 
403
        self.data.append(data)
 
404
        return len(self.data) - 1  # return the "index" of the data set
 
405
 
 
406
    def data_to_url(self, data_class=None):
 
407
        if not data_class:
 
408
            data_class = self.data_class_detection(self.data)
 
409
        if not issubclass(data_class, Data):
 
410
            raise UnknownDataTypeException()
 
411
        return repr(data_class(self.data))
 
412
 
 
413
    # Axis Labels
 
414
    # -------------------------------------------------------------------------
 
415
 
 
416
    def set_axis_labels(self, axis_type, values):
 
417
        assert(axis_type in Axis.TYPES)
 
418
        values = [ urllib.quote(a) for a in values ]
 
419
        axis_index = len(self.axis)
 
420
        axis = LabelAxis(axis_index, axis_type, values)
 
421
        self.axis.append(axis)
 
422
        return axis_index
 
423
 
 
424
    def set_axis_range(self, axis_type, low, high):
 
425
        assert(axis_type in Axis.TYPES)
 
426
        axis_index = len(self.axis)
 
427
        axis = RangeAxis(axis_index, axis_type, low, high)
 
428
        self.axis.append(axis)
 
429
        return axis_index
 
430
 
 
431
    def set_axis_positions(self, axis_index, positions):
 
432
        try:
 
433
            self.axis[axis_index].set_positions(positions)
 
434
        except IndexError:
 
435
            raise InvalidParametersException('Axis index %i has not been ' \
 
436
                'created' % axis)
 
437
 
 
438
    def set_axis_style(self, axis_index, colour, font_size=None, \
 
439
            alignment=None):
 
440
        try:
 
441
            self.axis[axis_index].set_style(colour, font_size, alignment)
 
442
        except IndexError:
 
443
            raise InvalidParametersException('Axis index %i has not been ' \
 
444
                'created' % axis)
 
445
 
 
446
    def axis_to_url(self):
 
447
        available_axis = []
 
448
        label_axis = []
 
449
        range_axis = []
 
450
        positions = []
 
451
        styles = []
 
452
        index = -1
 
453
        for axis in self.axis:
 
454
            available_axis.append(axis.axis_type)
 
455
            if isinstance(axis, RangeAxis):
 
456
                range_axis.append(repr(axis))
 
457
            if isinstance(axis, LabelAxis):
 
458
                label_axis.append(repr(axis))
 
459
            if axis.positions:
 
460
                positions.append(axis.positions_to_url())
 
461
            if axis.has_style:
 
462
                styles.append(axis.style_to_url())
 
463
        if not available_axis:
 
464
            return
 
465
        url_bits = []
 
466
        url_bits.append('chxt=%s' % ','.join(available_axis))
 
467
        if label_axis:
 
468
            url_bits.append('chxl=%s' % '|'.join(label_axis))
 
469
        if range_axis:
 
470
            url_bits.append('chxr=%s' % '|'.join(range_axis))
 
471
        if positions:
 
472
            url_bits.append('chxp=%s' % '|'.join(positions))
 
473
        if styles:
 
474
            url_bits.append('chxs=%s' % '|'.join(styles))
 
475
        return '&'.join(url_bits)
 
476
 
 
477
    # Markers, Ranges and Fill area (chm)
 
478
    # -------------------------------------------------------------------------
 
479
 
 
480
    def markers_to_url(self):
 
481
        return 'chm=%s' % '|'.join([','.join(a) for a in self.markers])
 
482
 
 
483
    def add_marker(self, index, point, marker_type, colour, size):
 
484
        self.markers.append((marker_type, colour, str(index), str(point), \
 
485
            str(size)))
 
486
 
 
487
    def add_horizontal_range(self, colour, start, stop):
 
488
        self.markers.append(('r', colour, '1', str(start), str(stop)))
 
489
 
 
490
    def add_vertical_range(self, colour, start, stop):
 
491
        self.markers.append(('R', colour, '1', str(start), str(stop)))
 
492
 
 
493
    def add_fill_range(self, colour, index_start, index_end):
 
494
        self.markers.append(('b', colour, str(index_start), str(index_end), \
 
495
            '1'))
 
496
 
 
497
    def add_fill_simple(self, colour):
 
498
        self.markers.append(('B', colour, '1', '1', '1'))
 
499
 
 
500
 
 
501
class ScatterChart(Chart):
 
502
 
 
503
    def __init__(self, *args, **kwargs):
 
504
        Chart.__init__(self, *args, **kwargs)
 
505
 
 
506
    def type_to_url(self):
 
507
        return 'cht=s'
 
508
 
 
509
 
 
510
class LineChart(Chart):
 
511
 
 
512
    def __init__(self, *args, **kwargs):
 
513
        assert(type(self) != LineChart)  # This is an abstract class
 
514
        Chart.__init__(self, *args, **kwargs)
 
515
        self.line_styles = {}
 
516
        self.grid = None
 
517
 
 
518
    def set_line_style(self, index, thickness=1, line_segment=None, \
 
519
            blank_segment=None):
 
520
        value = []
 
521
        value.append(str(thickness))
 
522
        if line_segment:
 
523
            value.append(str(line_segment))
 
524
            value.append(str(blank_segment))
 
525
        self.line_styles[index] = value
 
526
 
 
527
    def set_grid(self, x_step, y_step, line_segment=1, \
 
528
            blank_segment=0):
 
529
        self.grid = '%s,%s,%s,%s' % (x_step, y_step, line_segment, \
 
530
            blank_segment)
 
531
 
 
532
    def get_url_bits(self):
 
533
        url_bits = Chart.get_url_bits(self)
 
534
        if self.line_styles:
 
535
            style = []
 
536
            # for index, values in self.line_style.items():
 
537
            for index in xrange(max(self.line_styles) + 1):
 
538
                if index in self.line_styles:
 
539
                    values = self.line_styles[index]
 
540
                else:
 
541
                    values = ('1', )
 
542
                style.append(','.join(values))
 
543
            url_bits.append('chls=%s' % '|'.join(style))
 
544
        if self.grid:
 
545
            url_bits.append('chg=%s' % self.grid)
 
546
        return url_bits
 
547
 
 
548
 
 
549
class SimpleLineChart(LineChart):
 
550
 
 
551
    def type_to_url(self):
 
552
        return 'cht=lc'
 
553
 
 
554
 
 
555
class XYLineChart(LineChart):
 
556
 
 
557
    def type_to_url(self):
 
558
        return 'cht=lxy'
 
559
 
 
560
 
 
561
class BarChart(Chart):
 
562
 
 
563
    def __init__(self, *args, **kwargs):
 
564
        assert(type(self) != BarChart)  # This is an abstract class
 
565
        Chart.__init__(self, *args, **kwargs)
 
566
        self.bar_width = None
 
567
 
 
568
    def set_bar_width(self, bar_width):
 
569
        self.bar_width = bar_width
 
570
 
 
571
    def get_url_bits(self):
 
572
        url_bits = Chart.get_url_bits(self)
 
573
        url_bits.append('chbh=%i' % self.bar_width)
 
574
        return url_bits
 
575
 
 
576
 
 
577
class StackedHorizontalBarChart(BarChart):
 
578
 
 
579
    def type_to_url(self):
 
580
        return 'cht=bhs'
 
581
 
 
582
 
 
583
class StackedVerticalBarChart(BarChart):
 
584
 
 
585
    def type_to_url(self):
 
586
        return 'cht=bvs'
 
587
 
 
588
 
 
589
class GroupedBarChart(BarChart):
 
590
 
 
591
    def __init__(self, *args, **kwargs):
 
592
        assert(type(self) != GroupedBarChart)  # This is an abstract class
 
593
        BarChart.__init__(self, *args, **kwargs)
 
594
        self.bar_spacing = None
 
595
 
 
596
    def set_bar_spacing(self, spacing):
 
597
        self.bar_spacing = spacing
 
598
 
 
599
    def get_url_bits(self):
 
600
        # Skip 'BarChart.get_url_bits' and call Chart directly so the parent
 
601
        # doesn't add "chbh" before we do.
 
602
        url_bits = Chart.get_url_bits(self)
 
603
        if self.bar_spacing is not None:
 
604
            if self.bar_width is None:
 
605
                raise InvalidParametersException('Bar width is required to ' \
 
606
                    'be set when setting spacing')
 
607
            url_bits.append('chbh=%i,%i' % (self.bar_width, self.bar_spacing))
 
608
        else:
 
609
            url_bits.append('chbh=%i' % self.bar_width)
 
610
        return url_bits
 
611
 
 
612
 
 
613
class GroupedHorizontalBarChart(GroupedBarChart):
 
614
 
 
615
    def type_to_url(self):
 
616
        return 'cht=bhg'
 
617
 
 
618
 
 
619
class GroupedVerticalBarChart(GroupedBarChart):
 
620
 
 
621
    def type_to_url(self):
 
622
        return 'cht=bvg'
 
623
 
 
624
 
 
625
class PieChart(Chart):
 
626
 
 
627
    def __init__(self, *args, **kwargs):
 
628
        assert(type(self) != PieChart)  # This is an abstract class
 
629
        Chart.__init__(self, *args, **kwargs)
 
630
        self.pie_labels = []
 
631
 
 
632
    def set_pie_labels(self, labels):
 
633
        self.pie_labels = labels
 
634
 
 
635
    def get_url_bits(self):
 
636
        url_bits = Chart.get_url_bits(self)
 
637
        if self.pie_labels:
 
638
            url_bits.append('chl=%s' % '|'.join(self.pie_labels))
 
639
        return url_bits
 
640
 
 
641
 
 
642
class PieChart2D(PieChart):
 
643
 
 
644
    def type_to_url(self):
 
645
        return 'cht=p'
 
646
 
 
647
 
 
648
class PieChart3D(PieChart):
 
649
 
 
650
    def type_to_url(self):
 
651
        return 'cht=p3'
 
652
 
 
653
 
 
654
class VennChart(Chart):
 
655
 
 
656
    def type_to_url(self):
 
657
        return 'cht=v'
 
658
 
 
659
 
 
660
def test():
 
661
    chart = GroupedVerticalBarChart(320, 200)
 
662
    chart = PieChart2D(320, 200)
 
663
    chart = ScatterChart(320, 200)
 
664
    chart = SimpleLineChart(320, 200)
 
665
    sine_data = [math.sin(float(a) / 10) * 2000 + 2000 for a in xrange(100)]
 
666
    random_data = [a * random.random() * 30 for a in xrange(40)]
 
667
    random_data2 = [random.random() * 4000 for a in xrange(10)]
 
668
#    chart.set_bar_width(50)
 
669
#    chart.set_bar_spacing(0)
 
670
    chart.add_data(sine_data)
 
671
    chart.add_data(random_data)
 
672
    chart.add_data(random_data2)
 
673
#    chart.set_line_style(1, thickness=2)
 
674
#    chart.set_line_style(2, line_segment=10, blank_segment=5)
 
675
#    chart.set_title('heloooo')
 
676
#    chart.set_legend(('sine wave', 'random * x'))
 
677
#    chart.set_colours(('ee2000', 'DDDDAA', 'fF03f2'))
 
678
#    chart.fill_solid(Chart.BACKGROUND, '123456')
 
679
#    chart.fill_linear_gradient(Chart.CHART, 20, '004070', 1, '300040', 0,
 
680
#        'aabbcc00', 0.5)
 
681
#    chart.fill_linear_stripes(Chart.CHART, 20, '204070', .2, '300040', .2,
 
682
#        'aabbcc00', 0.2)
 
683
    axis_left_index = chart.set_axis_range(Axis.LEFT, 0, 10)
 
684
    axis_left_index = chart.set_axis_range(Axis.LEFT, 0, 10)
 
685
    axis_left_index = chart.set_axis_range(Axis.LEFT, 0, 10)
 
686
    axis_right_index = chart.set_axis_range(Axis.RIGHT, 5, 30)
 
687
    axis_bottom_index = chart.set_axis_labels(Axis.BOTTOM, [1, 25, 95])
 
688
    chart.set_axis_positions(axis_bottom_index, [1, 25, 95])
 
689
    chart.set_axis_style(axis_bottom_index, '003050', 15)
 
690
 
 
691
#    chart.set_pie_labels(('apples', 'oranges', 'bananas'))
 
692
 
 
693
#    chart.set_grid(10, 10)
 
694
 
 
695
#    for a in xrange(0, 100, 10):
 
696
#        chart.add_marker(1, a, 'a', 'AACA20', 10)
 
697
 
 
698
    chart.add_horizontal_range('00A020', .2, .5)
 
699
    chart.add_vertical_range('00c030', .2, .4)
 
700
 
 
701
    chart.add_fill_simple('303030A0')
 
702
 
 
703
    chart.download('test.png')
 
704
 
 
705
    url = chart.get_url()
 
706
    print url
 
707
    if 0:
 
708
        data = urllib.urlopen(chart.get_url()).read()
 
709
        open('meh.png', 'wb').write(data)
 
710
        os.system('start meh.png')
 
711
 
 
712
 
 
713
if __name__ == '__main__':
 
714
    test()