/+junk/pygooglechart-py3k

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