/+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-09 07:02:19 UTC
  • Revision ID: git-v1:f785cb66cf0f48566e497c0b771606362f32e709
initial import

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
import os
 
2
import urllib
 
3
import math
 
4
import random
 
5
import re
 
6
 
 
7
# Helper variables and functions
 
8
# -----------------------------------------------------------------------------
 
9
 
 
10
reo_colour = re.compile('^([A-Fa-f0-9]{2,2}){3,4}$')
 
11
 
 
12
def _check_colour(colour):
 
13
    if not reo_colour.match(colour):
 
14
        raise InvalidParametersException('Colours need to be in ' \
 
15
            'RRGGBB or RRGGBBAA format. One of your colours has %s' % \
 
16
            colour)
 
17
 
 
18
# Exception Classes
 
19
# -----------------------------------------------------------------------------
 
20
 
 
21
class PyGoogleChartException(Exception):
 
22
    pass
 
23
 
 
24
class DataOutOfRangeException(PyGoogleChartException):
 
25
    pass
 
26
 
 
27
class UnknownDataTypeException(PyGoogleChartException):
 
28
    pass
 
29
 
 
30
class NoDataGivenException(PyGoogleChartException):
 
31
    pass
 
32
 
 
33
class InvalidParametersException(PyGoogleChartException):
 
34
    pass
 
35
 
 
36
# Data Classes
 
37
# -----------------------------------------------------------------------------
 
38
 
 
39
class Data(object):
 
40
    def __init__(self, data):
 
41
        assert(type(self) != Data)  # This is an abstract class
 
42
        self.data = data
 
43
 
 
44
class SimpleData(Data):
 
45
    enc_map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
 
46
    def __repr__(self):
 
47
        encoded_data = []
 
48
        for data in self.data:
 
49
            sub_data = []
 
50
            for value in data:
 
51
                if value is None:
 
52
                    sub_data.append('_')
 
53
                elif value >= 0 and value <= SimpleData.max_value:
 
54
                    sub_data.append(SimpleData.enc_map[value])
 
55
                else:
 
56
                    raise DataOutOfRangeException()
 
57
            encoded_data.append(''.join(sub_data))
 
58
        return 'chd=s:' + ','.join(encoded_data)
 
59
    @staticmethod
 
60
    def max_value():
 
61
        return 61
 
62
 
 
63
class TextData(Data):
 
64
    def __repr__(self):
 
65
        encoded_data = []
 
66
        for data in self.data:
 
67
            sub_data = []
 
68
            for value in data:
 
69
                if value is None:
 
70
                    sub_data.append(-1)
 
71
                elif value >= 0 and value <= TextData.max_value:
 
72
                    sub_data.append(str(float(value)))
 
73
                else:
 
74
                    raise DataOutOfRangeException()
 
75
            encoded_data.append(','.join(sub_data))
 
76
        return 'chd=t:' + '|'.join(encoded_data)
 
77
    @staticmethod
 
78
    def max_value():
 
79
        return 100
 
80
 
 
81
class ExtendedData(Data):
 
82
    enc_map = \
 
83
        'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-.'
 
84
    def __repr__(self):
 
85
        encoded_data = []
 
86
        enc_size = len(ExtendedData.enc_map)
 
87
        for data in self.data:
 
88
            sub_data = []
 
89
            for value in data:
 
90
                if value is None:
 
91
                    sub_data.append('__')
 
92
                elif value >= 0 and value <= ExtendedData.max_value:
 
93
                    first, second = divmod(int(value), enc_size)
 
94
                    sub_data.append('%s%s' % (
 
95
                        ExtendedData.enc_map[first],
 
96
                        ExtendedData.enc_map[second]))
 
97
                else:
 
98
                    raise DataOutOfRangeException( \
 
99
                        'Item #%i "%s" is out of range' % (data.index(value), \
 
100
                        value))
 
101
            encoded_data.append(''.join(sub_data))
 
102
        return 'chd=e:' + ','.join(encoded_data)
 
103
    @staticmethod
 
104
    def max_value():
 
105
        return 4095
 
106
 
 
107
# Axis Classes
 
108
# -----------------------------------------------------------------------------
 
109
 
 
110
class Axis(object):
 
111
    BOTTOM = 'x'
 
112
    TOP = 't'
 
113
    LEFT = 'y'
 
114
    RIGHT = 'r'
 
115
    TYPES = (BOTTOM, TOP, LEFT, RIGHT)
 
116
    def __init__(self, axis, **kw):
 
117
        assert(axis in Axis.TYPES)
 
118
        self.has_style = False
 
119
        self.index = None
 
120
        self.positions = None
 
121
    def set_index(self, index):
 
122
        self.index = index
 
123
    def set_positions(self, positions):
 
124
        self.positions = positions
 
125
    def set_style(self, colour, font_size=None, alignment=None):
 
126
        _check_colour(colour)
 
127
        self.colour = colour
 
128
        self.font_size = font_size
 
129
        self.alignment = alignment
 
130
        self.has_style = True
 
131
    def style_to_url(self):
 
132
        bits = []
 
133
        bits.append(str(self.index))
 
134
        bits.append(self.colour)
 
135
        if self.font_size is not None:
 
136
            bits.append(str(self.font_size))
 
137
            if self.alignment is not None:
 
138
                bits.append(str(self.alignment))
 
139
        return ','.join(bits)
 
140
    def positions_to_url(self):
 
141
        bits = []
 
142
        bits.append(str(self.index))
 
143
        bits += [ str(a) for a in self.positions ]
 
144
        return ','.join(bits)
 
145
 
 
146
class LabelAxis(Axis):
 
147
    def __init__(self, axis, values, **kwargs):
 
148
        Axis.__init__(self, axis, **kwargs)
 
149
        self.values = [ str(a) for a in values ]
 
150
    def __repr__(self):
 
151
        return '%i:|%s' % (self.index, '|'.join(self.values))
 
152
 
 
153
class RangeAxis(Axis):
 
154
    def __init__(self, axis, low, high, **kwargs):
 
155
        Axis.__init__(self, axis, **kwargs)
 
156
        self.low = low
 
157
        self.high = high
 
158
    def __repr__(self):
 
159
        return '%i,%s,%s' % (self.index, self.low, self.high)
 
160
 
 
161
# Chart Classes
 
162
# -----------------------------------------------------------------------------
 
163
 
 
164
class Chart(object):
 
165
 
 
166
    BASE_URL = 'http://chart.apis.google.com/chart?'
 
167
    BACKGROUND = 'bg'
 
168
    CHART = 'c'
 
169
    SOLID = 's'
 
170
    LINEAR_GRADIENT = 'lg'
 
171
    LINEAR_STRIPES = 'ls'
 
172
 
 
173
    def __init__(self, width, height, title=None, legend=None, colours=None):
 
174
        assert(type(self) != Chart)  # This is an abstract class
 
175
        assert(isinstance(width, int))
 
176
        assert(isinstance(height, int))
 
177
        self.width = width
 
178
        self.height = height
 
179
        self.data = []
 
180
        self.set_title(title)
 
181
        self.set_legend(legend)
 
182
        self.set_colours(colours)
 
183
        self.fill_types = {
 
184
            Chart.BACKGROUND: None,
 
185
            Chart.CHART: None,
 
186
        }
 
187
        self.fill_area = {
 
188
            Chart.BACKGROUND: None,
 
189
            Chart.CHART: None,
 
190
        }
 
191
        self.axis = {
 
192
            Axis.TOP: None,
 
193
            Axis.BOTTOM: None,
 
194
            Axis.LEFT: None,
 
195
            Axis.RIGHT: None,
 
196
        }
 
197
        self.markers = []
 
198
 
 
199
    # URL generation
 
200
    # -------------------------------------------------------------------------
 
201
 
 
202
    def get_url(self):
 
203
        url_bits = self.get_url_bits()
 
204
        return self.BASE_URL + '&'.join(url_bits)
 
205
 
 
206
    def get_url_bits(self):
 
207
        url_bits = []
 
208
        # required arguments
 
209
        url_bits.append(self.type_to_url())
 
210
        url_bits.append('chs=%ix%i' % (self.width, self.height))
 
211
        url_bits.append(self.data_to_url())
 
212
        # optional arguments
 
213
        if self.title:
 
214
            url_bits.append('chtt=%s' % self.title)
 
215
        if self.legend:
 
216
            url_bits.append('chdl=%s' % '|'.join(self.legend))
 
217
        if self.colours:
 
218
            url_bits.append('chco=%s' % ','.join(self.colours))
 
219
        ret = self.fill_to_url()
 
220
        if ret:
 
221
            url_bits.append(ret)
 
222
        ret = self.axis_to_url()
 
223
        if ret:
 
224
            url_bits.append(ret)
 
225
        if self.markers:
 
226
            url_bits.append(self.markers_to_url())
 
227
        return url_bits
 
228
 
 
229
    # Simple settings
 
230
    # -------------------------------------------------------------------------
 
231
 
 
232
    def set_title(self, title):
 
233
        if title:
 
234
            self.title = urllib.quote(title)
 
235
        else:
 
236
            self.title = None
 
237
 
 
238
    def set_legend(self, legend):
 
239
        # legend needs to be a list, tuple or None
 
240
        assert(isinstance(legend, list) or isinstance(legend, tuple) or
 
241
            legend is None)
 
242
        if legend:
 
243
            self.legend = [ urllib.quote(a) for a in legend ]
 
244
        else:
 
245
            self.legend = None
 
246
 
 
247
    # Chart colours
 
248
    # -------------------------------------------------------------------------
 
249
 
 
250
    def set_colours(self, colours):
 
251
        # colours needs to be a list, tuple or None
 
252
        assert(isinstance(colours, list) or isinstance(colours, tuple) or
 
253
            colours is None)
 
254
        # make sure the colours are in the right format
 
255
        if colours:
 
256
            for col in colours:
 
257
                _check_colour(col)
 
258
        self.colours = colours
 
259
 
 
260
    # Background/Chart colours
 
261
    # -------------------------------------------------------------------------
 
262
 
 
263
    def fill_solid(self, area, colour):
 
264
        assert(area in (Chart.BACKGROUND, Chart.CHART))
 
265
        _check_colour(colour)
 
266
        self.fill_area[area] = colour
 
267
        self.fill_types[area] = Chart.SOLID
 
268
 
 
269
    def _check_fill_linear(self, angle, *args):
 
270
        assert(isinstance(args, list) or isinstance(args, tuple))
 
271
        assert(angle >= 0 and angle <= 90)
 
272
        assert(len(args) % 2 == 0)
 
273
        args = list(args)  # args is probably a tuple and we need to mutate
 
274
        for a in xrange(len(args) / 2):
 
275
            col = args[a * 2]
 
276
            offset = args[a * 2 + 1]
 
277
            _check_colour(col)
 
278
            assert(offset >= 0 and offset <= 1)
 
279
            args[a * 2 + 1] = str(args[a * 2 + 1])
 
280
        return args
 
281
 
 
282
    def fill_linear_gradient(self, area, angle, *args):
 
283
        assert(area in (Chart.BACKGROUND, Chart.CHART))
 
284
        args = self._check_fill_linear(angle, *args)
 
285
        self.fill_types[area] = Chart.LINEAR_GRADIENT
 
286
        self.fill_area[area] = ','.join([str(angle)] + args)
 
287
 
 
288
    def fill_linear_stripes(self, area, angle, *args):
 
289
        assert(area in (Chart.BACKGROUND, Chart.CHART))
 
290
        args = self._check_fill_linear(angle, *args)
 
291
        self.fill_types[area] = Chart.LINEAR_STRIPES
 
292
        self.fill_area[area] = ','.join([str(angle)] + args)
 
293
 
 
294
    def fill_to_url(self):
 
295
        areas = []
 
296
        for area in (Chart.BACKGROUND, Chart.CHART):
 
297
            if self.fill_types[area]:
 
298
                areas.append('%s,%s,%s' % (area, self.fill_types[area], \
 
299
                    self.fill_area[area]))
 
300
        if areas:
 
301
            return 'chf=' + '|'.join(areas)
 
302
 
 
303
    # Data
 
304
    # -------------------------------------------------------------------------
 
305
 
 
306
    def data_class_detection(self, data):
 
307
        """
 
308
        Detects and returns the data type required based on the range of the
 
309
        data given. The data given must be lists of numbers within a list.
 
310
        """
 
311
        assert(isinstance(data, list) or isinstance(data, tuple))
 
312
        max_value = None
 
313
        for a in data:
 
314
            assert(isinstance(a, list) or isinstance(a, tuple))
 
315
            if max_value is None or max(a) > max_value:
 
316
                max_value = max(a)
 
317
        for data_class in (SimpleData, TextData, ExtendedData):
 
318
            if max_value <= data_class.max_value():
 
319
                return data_class
 
320
        raise DataOutOfRangeException()
 
321
 
 
322
    def add_data(self, data):
 
323
        self.data.append(data)
 
324
 
 
325
    def data_to_url(self, data_class=None):
 
326
        if not data_class:
 
327
            data_class = self.data_class_detection(self.data)
 
328
        if not issubclass(data_class, Data):
 
329
            raise UnknownDataTypeException()
 
330
        return repr(data_class(self.data))
 
331
 
 
332
    # Axis Labels
 
333
    # -------------------------------------------------------------------------
 
334
 
 
335
    def set_axis_labels(self, axis, values):
 
336
        assert(axis in Axis.TYPES)
 
337
        self.axis[axis] = LabelAxis(axis, values)
 
338
 
 
339
    def set_axis_range(self, axis, low, high):
 
340
        assert(axis in Axis.TYPES)
 
341
        self.axis[axis] = RangeAxis(axis, low, high)
 
342
 
 
343
    def set_axis_positions(self, axis, positions):
 
344
        assert(axis in Axis.TYPES)
 
345
        if not self.axis[axis]:
 
346
            raise InvalidParametersException('Please create an axis first')
 
347
        self.axis[axis].set_positions(positions)
 
348
 
 
349
    def set_axis_style(self, axis, colour, font_size=None, alignment=None):
 
350
        assert(axis in Axis.TYPES)
 
351
        if not self.axis[axis]:
 
352
            raise InvalidParametersException('Please create an axis first')
 
353
        self.axis[axis].set_style(colour, font_size, alignment)
 
354
 
 
355
    def axis_to_url(self):
 
356
        available_axis = []
 
357
        label_axis = []
 
358
        range_axis = []
 
359
        positions = []
 
360
        styles = []
 
361
        index = -1
 
362
        for position, axis in self.axis.items():
 
363
            if not axis:
 
364
                continue
 
365
            index += 1
 
366
            axis.set_index(index)
 
367
            available_axis.append(position)
 
368
            if isinstance(axis, RangeAxis):
 
369
                range_axis.append(repr(axis))
 
370
            if isinstance(axis, LabelAxis):
 
371
                label_axis.append(repr(axis))
 
372
            if axis.positions:
 
373
                positions.append(axis.positions_to_url())
 
374
            if axis.has_style:
 
375
                styles.append(axis.style_to_url())
 
376
        if not available_axis:
 
377
            return
 
378
        url_bits = []
 
379
        url_bits.append('chxt=%s' % ','.join(available_axis))
 
380
        if label_axis:
 
381
            url_bits.append('chxl=%s' % '|'.join(label_axis))
 
382
        if range_axis:
 
383
            url_bits.append('chxr=%s' % '|'.join(range_axis))
 
384
        if positions:
 
385
            url_bits.append('chxp=%s' % '|'.join(positions))
 
386
        if styles:
 
387
            url_bits.append('chxs=%s' % '|'.join(styles))
 
388
        return '&'.join(url_bits)
 
389
 
 
390
    # Markers, Ranges and Fill area (chm)
 
391
    # -------------------------------------------------------------------------
 
392
 
 
393
    def markers_to_url(self):
 
394
        print 'chm=%s' % '|'.join([ ','.join(a) for a in self.markers ])
 
395
        return 'chm=%s' % '|'.join([ ','.join(a) for a in self.markers ])
 
396
 
 
397
    def add_marker(self, index, point, marker_type, colour, size):
 
398
        self.markers.append((marker_type, colour, str(index), str(point), \
 
399
            str(size)))
 
400
 
 
401
    def add_horizontal_range(self, colour, start, stop):
 
402
        self.markers.append(('r', colour, '1', str(start), str(stop)))
 
403
 
 
404
    def add_vertical_range(self, colour, start, stop):
 
405
        self.markers.append(('R', colour, '1', str(start), str(stop)))
 
406
 
 
407
    def add_fill_range(self, colour, index_start, index_end):
 
408
        self.markers.append(('b', colour, str(index_start), str(index_end), \
 
409
            '1'))
 
410
 
 
411
    def add_fill_simple(self, colour):
 
412
        self.markers.append(('B', colour, '1', '1', '1'))
 
413
 
 
414
class ScatterChart(Chart):
 
415
    def __init__(self, *args, **kwargs):
 
416
        Chart.__init__(self, *args, **kwargs)
 
417
    def type_to_url(self):
 
418
        return 'cht=s'
 
419
 
 
420
 
 
421
class LineChart(Chart):
 
422
    def __init__(self, *args, **kwargs):
 
423
        Chart.__init__(self, *args, **kwargs)
 
424
        self.line_styles = {}
 
425
        self.grid = None
 
426
    def set_line_style(self, index, thickness=1, line_segment=None, \
 
427
            blank_segment=None):
 
428
        value = []
 
429
        value.append(str(thickness))
 
430
        if line_segment:
 
431
            value.append(str(line_segment))
 
432
            value.append(str(blank_segment))
 
433
        self.line_styles[index] = value
 
434
    def set_grid(self, x_step, y_step, line_segment=1, \
 
435
            blank_segment=0):
 
436
        self.grid = '%s,%s,%s,%s' % (x_step, y_step, line_segment, \
 
437
            blank_segment)
 
438
    def get_url_bits(self):
 
439
        url_bits = Chart.get_url_bits(self)
 
440
        if self.line_styles:
 
441
            style = []
 
442
            # for index, values in self.line_style.items():
 
443
            for index in xrange(max(self.line_styles) + 1):
 
444
                if index in self.line_styles:
 
445
                    values = self.line_styles[index]
 
446
                else:
 
447
                    values = ('1', )
 
448
                style.append(','.join(values))
 
449
            url_bits.append('chls=%s' % '|'.join(style))
 
450
        if self.grid:
 
451
            url_bits.append('chg=%s' % self.grid)
 
452
        return url_bits
 
453
 
 
454
class SimpleLineChart(LineChart):
 
455
    def type_to_url(self):
 
456
        return 'cht=lc'
 
457
 
 
458
class XYLineChart(LineChart):
 
459
    def type_to_url(self):
 
460
        return 'cht=lxy'
 
461
 
 
462
class BarChart(Chart):
 
463
    def __init__(self, *args, **kwargs):
 
464
        assert(type(self) != BarChart)  # This is an abstract class
 
465
        Chart.__init__(self, *args, **kwargs)
 
466
        self.bar_width = None
 
467
    def set_bar_width(self, bar_width):
 
468
        self.bar_width = bar_width
 
469
    def get_url_bits(self):
 
470
        url_bits = Chart.get_url_bits(self)
 
471
        url_bits.append('chbh=%i' % self.bar_width)
 
472
        return url_bits
 
473
 
 
474
class StackedHorizontalBarChart(BarChart):
 
475
    def type_to_url(self):
 
476
        return 'cht=bhs'
 
477
 
 
478
class StackedVerticalBarChart(BarChart):
 
479
    def type_to_url(self):
 
480
        return 'cht=bvs'
 
481
 
 
482
class GroupedBarChart(BarChart):
 
483
    def __init__(self, *args, **kwargs):
 
484
        assert(type(self) != GroupedBarChart)  # This is an abstract class
 
485
        BarChart.__init__(self, *args, **kwargs)
 
486
        self.bar_spacing = None
 
487
    def set_bar_spacing(self, spacing):
 
488
        self.bar_spacing = spacing
 
489
    def get_url_bits(self):
 
490
        # Skip 'BarChart.get_url_bits' and call Chart directly so the parent
 
491
        # doesn't add "chbh" before we do.
 
492
        url_bits = Chart.get_url_bits(self)
 
493
        if self.bar_spacing is not None:
 
494
            if self.bar_width is None:
 
495
                raise InvalidParametersException('Bar width is required to ' \
 
496
                    'be set when setting spacing')
 
497
            url_bits.append('chbh=%i,%i' % (self.bar_width, self.bar_spacing))
 
498
        else:
 
499
            url_bits.append('chbh=%i' % self.bar_width)
 
500
        return url_bits
 
501
 
 
502
class GroupedHorizontalBarChart(GroupedBarChart):
 
503
    def type_to_url(self):
 
504
        return 'cht=bhg'
 
505
 
 
506
class GroupedVerticalBarChart(GroupedBarChart):
 
507
    def type_to_url(self):
 
508
        return 'cht=bvg'
 
509
 
 
510
class PieChart(Chart):
 
511
    def __init__(self, *args, **kwargs):
 
512
        assert(type(self) != PieChart)  # This is an abstract class
 
513
        Chart.__init__(self, *args, **kwargs)
 
514
        self.pie_labels = []
 
515
    def set_pie_labels(self, labels):
 
516
        self.pie_labels = labels
 
517
    def get_url_bits(self):
 
518
        url_bits = Chart.get_url_bits(self)
 
519
        if self.pie_labels:
 
520
            url_bits.append('chl=%s' % '|'.join(self.pie_labels))
 
521
        return url_bits
 
522
 
 
523
class PieChart2D(PieChart):
 
524
    def type_to_url(self):
 
525
        return 'cht=p'
 
526
 
 
527
class PieChart3D(PieChart):
 
528
    def type_to_url(self):
 
529
        return 'cht=p3'
 
530
 
 
531
class VennChart(Chart):
 
532
    def type_to_url(self):
 
533
        return 'cht=v'
 
534
 
 
535
def test():
 
536
    chart = GroupedVerticalBarChart(320, 200)
 
537
    chart = PieChart2D(320, 200)
 
538
    chart = ScatterChart(320, 200)
 
539
    chart = SimpleLineChart(320, 200)
 
540
    sine_data = [ math.sin(float(a) / 10) * 2000 + 2000 for a in xrange(100) ]
 
541
    random_data = [ a * random.random() * 30 for a in xrange(40) ]
 
542
    random_data2 = [ random.random() * 4000 for a in xrange(10) ]
 
543
#    chart.set_bar_width(50)
 
544
#    chart.set_bar_spacing(0)
 
545
    chart.add_data(sine_data)
 
546
    chart.add_data(random_data)
 
547
    chart.add_data(random_data2)
 
548
#    chart.set_line_style(1, thickness=2)
 
549
#    chart.set_line_style(2, line_segment=10, blank_segment=5)
 
550
#    chart.set_title('heloooo')
 
551
#    chart.set_legend(('sine wave', 'random * x'))
 
552
#    chart.set_colours(('ee2000', 'DDDDAA', 'fF03f2'))
 
553
#    chart.fill_solid(Chart.BACKGROUND, '123456')
 
554
#    chart.fill_linear_gradient(Chart.CHART, 20, '004070', 1, '300040', 0,
 
555
#        'aabbcc00', 0.5)
 
556
#    chart.fill_linear_stripes(Chart.CHART, 20, '204070', .2, '300040', .2,
 
557
#        'aabbcc00', 0.2)
 
558
    chart.set_axis_range(Axis.LEFT, 0, 10)
 
559
    chart.set_axis_range(Axis.RIGHT, 5, 30)
 
560
    chart.set_axis_labels(Axis.BOTTOM, [1, 25, 95])
 
561
    chart.set_axis_positions(Axis.BOTTOM, [1, 25, 95])
 
562
    chart.set_axis_style(Axis.BOTTOM, 'FFFFFF', 15)
 
563
 
 
564
#    chart.set_pie_labels(('apples', 'oranges', 'bananas'))
 
565
 
 
566
#    chart.set_grid(10, 10)
 
567
 
 
568
#    for a in xrange(0, 100, 10):
 
569
#        chart.add_marker(1, a, 'a', 'AACA20', 10)
 
570
 
 
571
    chart.add_horizontal_range('00A020', .2, .5)
 
572
    chart.add_vertical_range('00c030', .2, .4)
 
573
 
 
574
    chart.add_fill_simple('303030A0')
 
575
 
 
576
    url = chart.get_url()
 
577
    print url
 
578
    if 0:
 
579
        data = urllib.urlopen(chart.get_url()).read()
 
580
        open('meh.png', 'wb').write(data)
 
581
        os.system('start meh.png')
 
582
 
 
583
if __name__ == '__main__':
 
584
    test()
 
585