/+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-13 23:41:30 UTC
  • Revision ID: git-v1:cbcc1a47416f61cbbdfa772616ad526d04a1703b
removed debugging from an example

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