/+junk/pygooglechart-py3k

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