/+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: 2008-04-26 01:06:52 UTC
  • Revision ID: git-v1:2e9a4ccbb337ef7e85380191cd5c1c76efcd45f2
- pygooglechart.py converted to unix line breaks
- Added sparklines
- Minor code cleanup

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
"""
2
 
pygooglechart - A complete Python wrapper for the Google Chart API
 
2
PyGoogleChart - A complete Python wrapper for the Google Chart API
3
3
 
4
4
http://pygooglechart.slowchop.com/
5
5
 
6
 
Copyright 2007-2008 Gerald Kaszuba
 
6
Copyright 2007 Gerald Kaszuba
7
7
 
8
8
This program is free software: you can redistribute it and/or modify
9
9
it under the terms of the GNU General Public License as published by
19
19
along with this program.  If not, see <http://www.gnu.org/licenses/>.
20
20
 
21
21
"""
22
 
from __future__ import division
23
22
 
24
23
import os
25
24
import urllib
27
26
import math
28
27
import random
29
28
import re
30
 
import warnings
31
 
import copy
32
29
 
33
30
# Helper variables and functions
34
31
# -----------------------------------------------------------------------------
35
32
 
36
 
__version__ = '0.2.2'
37
 
__author__ = 'Gerald Kaszuba'
 
33
__version__ = '0.1.3'
38
34
 
39
35
reo_colour = re.compile('^([A-Fa-f0-9]{2,2}){3,4}$')
40
36
 
 
37
 
41
38
def _check_colour(colour):
42
39
    if not reo_colour.match(colour):
43
40
        raise InvalidParametersException('Colours need to be in ' \
44
41
            'RRGGBB or RRGGBBAA format. One of your colours has %s' % \
45
42
            colour)
46
43
 
47
 
 
48
 
def _reset_warnings():
49
 
    """Helper function to reset all warnings. Used by the unit tests."""
50
 
    globals()['__warningregistry__'] = None
51
 
 
52
 
 
53
44
# Exception Classes
54
45
# -----------------------------------------------------------------------------
55
46
 
78
69
    pass
79
70
 
80
71
 
81
 
class AbstractClassException(PyGoogleChartException):
82
 
    pass
83
 
 
84
 
 
85
 
class UnknownChartType(PyGoogleChartException):
86
 
    pass
87
 
 
88
 
 
89
72
# Data Classes
90
73
# -----------------------------------------------------------------------------
91
74
 
93
76
class Data(object):
94
77
 
95
78
    def __init__(self, data):
96
 
        if type(self) == Data:
97
 
            raise AbstractClassException('This is an abstract class')
 
79
        assert(type(self) != Data)  # This is an abstract class
98
80
        self.data = data
99
81
 
100
 
    @classmethod
101
 
    def float_scale_value(cls, value, range):
102
 
        lower, upper = range
103
 
        assert(upper > lower)
104
 
        scaled = (value - lower) * (cls.max_value / (upper - lower))
105
 
        return scaled
106
 
 
107
 
    @classmethod
108
 
    def clip_value(cls, value):
109
 
        return max(0, min(value, cls.max_value))
110
 
 
111
 
    @classmethod
112
 
    def int_scale_value(cls, value, range):
113
 
        return int(round(cls.float_scale_value(value, range)))
114
 
 
115
 
    @classmethod
116
 
    def scale_value(cls, value, range):
117
 
        scaled = cls.int_scale_value(value, range)
118
 
        clipped = cls.clip_value(scaled)
119
 
        Data.check_clip(scaled, clipped)
120
 
        return clipped
121
 
 
122
 
    @staticmethod
123
 
    def check_clip(scaled, clipped):
124
 
        if clipped != scaled:
125
 
            warnings.warn('One or more of of your data points has been '
126
 
                'clipped because it is out of range.')
127
 
 
128
82
 
129
83
class SimpleData(Data):
130
 
 
131
 
    max_value = 61
132
84
    enc_map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
133
85
 
134
86
    def __repr__(self):
 
87
        max_value = self.max_value()
135
88
        encoded_data = []
136
89
        for data in self.data:
137
90
            sub_data = []
138
91
            for value in data:
139
92
                if value is None:
140
93
                    sub_data.append('_')
141
 
                elif value >= 0 and value <= self.max_value:
 
94
                elif value >= 0 and value <= max_value:
142
95
                    sub_data.append(SimpleData.enc_map[value])
143
96
                else:
144
97
                    raise DataOutOfRangeException('cannot encode value: %d'
146
99
            encoded_data.append(''.join(sub_data))
147
100
        return 'chd=s:' + ','.join(encoded_data)
148
101
 
 
102
    @staticmethod
 
103
    def max_value():
 
104
        return 61
 
105
 
 
106
    @classmethod
 
107
    def scale_value(cls, value, range):
 
108
        lower, upper = range
 
109
        max_value = cls.max_value()
 
110
        scaled = int(round((float(value) - lower) * max_value / upper))
 
111
        clipped = max(0, min(scaled, max_value))
 
112
        return clipped
149
113
 
150
114
class TextData(Data):
151
115
 
152
 
    max_value = 100
153
 
 
154
116
    def __repr__(self):
 
117
        max_value = self.max_value()
155
118
        encoded_data = []
156
119
        for data in self.data:
157
120
            sub_data = []
158
121
            for value in data:
159
122
                if value is None:
160
123
                    sub_data.append(-1)
161
 
                elif value >= 0 and value <= self.max_value:
 
124
                elif value >= 0 and value <= max_value:
162
125
                    sub_data.append("%.1f" % float(value))
163
126
                else:
164
127
                    raise DataOutOfRangeException()
165
128
            encoded_data.append(','.join(sub_data))
166
 
        return 'chd=t:' + '%7c'.join(encoded_data)
 
129
        return 'chd=t:' + '|'.join(encoded_data)
 
130
 
 
131
    @staticmethod
 
132
    def max_value():
 
133
        return 100
167
134
 
168
135
    @classmethod
169
136
    def scale_value(cls, value, range):
170
 
        # use float values instead of integers because we don't need an encode
171
 
        # map index
172
 
        scaled = cls.float_scale_value(value, range)
173
 
        clipped = cls.clip_value(scaled)
174
 
        Data.check_clip(scaled, clipped)
 
137
        lower, upper = range
 
138
        max_value = cls.max_value()
 
139
        scaled = (float(value) - lower) * max_value / upper
 
140
        clipped = max(0, min(scaled, max_value))
175
141
        return clipped
176
142
 
177
 
 
178
143
class ExtendedData(Data):
179
 
 
180
 
    max_value = 4095
181
144
    enc_map = \
182
145
        'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-.'
183
146
 
184
147
    def __repr__(self):
 
148
        max_value = self.max_value()
185
149
        encoded_data = []
186
150
        enc_size = len(ExtendedData.enc_map)
187
151
        for data in self.data:
189
153
            for value in data:
190
154
                if value is None:
191
155
                    sub_data.append('__')
192
 
                elif value >= 0 and value <= self.max_value:
 
156
                elif value >= 0 and value <= max_value:
193
157
                    first, second = divmod(int(value), enc_size)
194
158
                    sub_data.append('%s%s' % (
195
159
                        ExtendedData.enc_map[first],
201
165
            encoded_data.append(''.join(sub_data))
202
166
        return 'chd=e:' + ','.join(encoded_data)
203
167
 
 
168
    @staticmethod
 
169
    def max_value():
 
170
        return 4095
 
171
 
 
172
    @classmethod
 
173
    def scale_value(cls, value, range):
 
174
        lower, upper = range
 
175
        max_value = cls.max_value()
 
176
        scaled = int(round((float(value) - lower) * max_value / upper))
 
177
        clipped = max(0, min(scaled, max_value))
 
178
        return clipped
 
179
 
204
180
 
205
181
# Axis Classes
206
182
# -----------------------------------------------------------------------------
207
183
 
208
184
 
209
185
class Axis(object):
210
 
 
211
186
    BOTTOM = 'x'
212
187
    TOP = 't'
213
188
    LEFT = 'y'
258
233
        self.values = [str(a) for a in values]
259
234
 
260
235
    def __repr__(self):
261
 
        return '%i:%%7c%s' % (self.axis_index, '%7c'.join(self.values))
 
236
        return '%i:|%s' % (self.axis_index, '|'.join(self.values))
262
237
 
263
238
 
264
239
class RangeAxis(Axis):
285
260
    BASE_URL = 'http://chart.apis.google.com/chart?'
286
261
    BACKGROUND = 'bg'
287
262
    CHART = 'c'
288
 
    ALPHA = 'a'
289
 
    VALID_SOLID_FILL_TYPES = (BACKGROUND, CHART, ALPHA)
290
263
    SOLID = 's'
291
264
    LINEAR_GRADIENT = 'lg'
292
265
    LINEAR_STRIPES = 'ls'
293
266
 
294
267
    def __init__(self, width, height, title=None, legend=None, colours=None,
295
 
            auto_scale=True, x_range=None, y_range=None,
296
 
            colours_within_series=None):
297
 
        if type(self) == Chart:
298
 
            raise AbstractClassException('This is an abstract class')
 
268
                 auto_scale=True, x_range=None, y_range=None):
 
269
        assert(type(self) != Chart)  # This is an abstract class
299
270
        assert(isinstance(width, int))
300
271
        assert(isinstance(height, int))
301
272
        self.width = width
302
273
        self.height = height
303
274
        self.data = []
304
275
        self.set_title(title)
305
 
        self.set_title_style(None, None)
306
276
        self.set_legend(legend)
307
 
        self.set_legend_position(None)
308
277
        self.set_colours(colours)
309
 
        self.set_colours_within_series(colours_within_series)
310
278
 
311
279
        # Data for scaling.
312
 
        self.auto_scale = auto_scale  # Whether to automatically scale data
313
 
        self.x_range = x_range  # (min, max) x-axis range for scaling
314
 
        self.y_range = y_range  # (min, max) y-axis range for scaling
 
280
        self.auto_scale = auto_scale    # Whether to automatically scale data
 
281
        self.x_range = x_range          # (min, max) x-axis range for scaling
 
282
        self.y_range = y_range          # (min, max) y-axis range for scaling
315
283
        self.scaled_data_class = None
316
284
        self.scaled_x_range = None
317
285
        self.scaled_y_range = None
319
287
        self.fill_types = {
320
288
            Chart.BACKGROUND: None,
321
289
            Chart.CHART: None,
322
 
            Chart.ALPHA: None,
323
290
        }
324
291
        self.fill_area = {
325
292
            Chart.BACKGROUND: None,
326
293
            Chart.CHART: None,
327
 
            Chart.ALPHA: None,
328
294
        }
329
295
        self.axis = []
330
296
        self.markers = []
331
 
        self.line_styles = {}
332
 
        self.grid = None
333
297
 
334
298
    # URL generation
335
299
    # -------------------------------------------------------------------------
336
300
 
337
 
    def get_url(self, data_class=None):
338
 
        url_bits = self.get_url_bits(data_class=data_class)
 
301
    def get_url(self):
 
302
        url_bits = self.get_url_bits()
339
303
        return self.BASE_URL + '&'.join(url_bits)
340
304
 
341
 
    def get_url_bits(self, data_class=None):
 
305
    def get_url_bits(self):
342
306
        url_bits = []
343
307
        # required arguments
344
308
        url_bits.append(self.type_to_url())
345
309
        url_bits.append('chs=%ix%i' % (self.width, self.height))
346
 
        url_bits.append(self.data_to_url(data_class=data_class))
 
310
        url_bits.append(self.data_to_url())
347
311
        # optional arguments
348
312
        if self.title:
349
313
            url_bits.append('chtt=%s' % self.title)
350
 
        if self.title_colour and self.title_font_size:
351
 
            url_bits.append('chts=%s,%s' % (self.title_colour, \
352
 
                self.title_font_size))
353
314
        if self.legend:
354
 
            url_bits.append('chdl=%s' % '%7c'.join(self.legend))
355
 
        if self.legend_position:
356
 
            url_bits.append('chdlp=%s' % (self.legend_position))
 
315
            url_bits.append('chdl=%s' % '|'.join(self.legend))
357
316
        if self.colours:
358
 
            url_bits.append('chco=%s' % ','.join(self.colours))            
359
 
        if self.colours_within_series:
360
 
            url_bits.append('chco=%s' % '%7c'.join(self.colours_within_series))
 
317
            url_bits.append('chco=%s' % ','.join(self.colours))
361
318
        ret = self.fill_to_url()
362
319
        if ret:
363
320
            url_bits.append(ret)
364
321
        ret = self.axis_to_url()
365
322
        if ret:
366
 
            url_bits.append(ret)                    
 
323
            url_bits.append(ret)
367
324
        if self.markers:
368
 
            url_bits.append(self.markers_to_url())        
369
 
        if self.line_styles:
370
 
            style = []
371
 
            for index in xrange(max(self.line_styles) + 1):
372
 
                if index in self.line_styles:
373
 
                    values = self.line_styles[index]
374
 
                else:
375
 
                    values = ('1', )
376
 
                style.append(','.join(values))
377
 
            url_bits.append('chls=%s' % '%7c'.join(style))
378
 
        if self.grid:
379
 
            url_bits.append('chg=%s' % self.grid)
 
325
            url_bits.append(self.markers_to_url())
380
326
        return url_bits
381
327
 
382
328
    # Downloading
389
335
            raise BadContentTypeException('Server responded with a ' \
390
336
                'content-type of %s' % opener.headers['content-type'])
391
337
 
392
 
        open(file_name, 'wb').write(opener.read())
 
338
        open(file_name, 'wb').write(urllib.urlopen(self.get_url()).read())
393
339
 
394
340
    # Simple settings
395
341
    # -------------------------------------------------------------------------
400
346
        else:
401
347
            self.title = None
402
348
 
403
 
    def set_title_style(self, colour, font_size):
404
 
        if not colour is None:
405
 
            _check_colour(colour)
406
 
        self.title_colour = colour
407
 
        self.title_font_size = font_size
408
 
 
409
349
    def set_legend(self, legend):
410
350
        """legend needs to be a list, tuple or None"""
411
351
        assert(isinstance(legend, list) or isinstance(legend, tuple) or
415
355
        else:
416
356
            self.legend = None
417
357
 
418
 
    def set_legend_position(self, legend_position):
419
 
        if legend_position:
420
 
            self.legend_position = urllib.quote(legend_position)
421
 
        else:    
422
 
            self.legend_position = None
423
 
 
424
358
    # Chart colours
425
359
    # -------------------------------------------------------------------------
426
360
 
434
368
                _check_colour(col)
435
369
        self.colours = colours
436
370
 
437
 
    def set_colours_within_series(self, colours):
438
 
        # colours needs to be a list, tuple or None
439
 
        assert(isinstance(colours, list) or isinstance(colours, tuple) or
440
 
            colours is None)
441
 
        # make sure the colours are in the right format
442
 
        if colours:
443
 
            for col in colours:
444
 
                _check_colour(col)
445
 
        self.colours_within_series = colours        
446
 
 
447
371
    # Background/Chart colours
448
372
    # -------------------------------------------------------------------------
449
373
 
450
374
    def fill_solid(self, area, colour):
451
 
        assert(area in Chart.VALID_SOLID_FILL_TYPES)
 
375
        assert(area in (Chart.BACKGROUND, Chart.CHART))
452
376
        _check_colour(colour)
453
377
        self.fill_area[area] = colour
454
378
        self.fill_types[area] = Chart.SOLID
458
382
        assert(angle >= 0 and angle <= 90)
459
383
        assert(len(args) % 2 == 0)
460
384
        args = list(args)  # args is probably a tuple and we need to mutate
461
 
        for a in xrange(int(len(args) / 2)):
 
385
        for a in xrange(len(args) / 2):
462
386
            col = args[a * 2]
463
387
            offset = args[a * 2 + 1]
464
388
            _check_colour(col)
467
391
        return args
468
392
 
469
393
    def fill_linear_gradient(self, area, angle, *args):
470
 
        assert(area in Chart.VALID_SOLID_FILL_TYPES)
 
394
        assert(area in (Chart.BACKGROUND, Chart.CHART))
471
395
        args = self._check_fill_linear(angle, *args)
472
396
        self.fill_types[area] = Chart.LINEAR_GRADIENT
473
397
        self.fill_area[area] = ','.join([str(angle)] + args)
474
398
 
475
399
    def fill_linear_stripes(self, area, angle, *args):
476
 
        assert(area in Chart.VALID_SOLID_FILL_TYPES)
 
400
        assert(area in (Chart.BACKGROUND, Chart.CHART))
477
401
        args = self._check_fill_linear(angle, *args)
478
402
        self.fill_types[area] = Chart.LINEAR_STRIPES
479
403
        self.fill_area[area] = ','.join([str(angle)] + args)
480
404
 
481
405
    def fill_to_url(self):
482
406
        areas = []
483
 
        for area in (Chart.BACKGROUND, Chart.CHART, Chart.ALPHA):
 
407
        for area in (Chart.BACKGROUND, Chart.CHART):
484
408
            if self.fill_types[area]:
485
409
                areas.append('%s,%s,%s' % (area, self.fill_types[area], \
486
410
                    self.fill_area[area]))
487
411
        if areas:
488
 
            return 'chf=' + '%7c'.join(areas)
 
412
            return 'chf=' + '|'.join(areas)
489
413
 
490
414
    # Data
491
415
    # -------------------------------------------------------------------------
506
430
            # simple encoding. I've found that this isn't sufficient,
507
431
            # e.g. examples/line-xy-circle.png. Let's try 100px.
508
432
            return SimpleData
 
433
        elif self.height < 500:
 
434
            return TextData
509
435
        else:
510
436
            return ExtendedData
511
437
 
512
 
    def _filter_none(self, data):
513
 
        return [r for r in data if r is not None]
514
 
 
515
438
    def data_x_range(self):
516
439
        """Return a 2-tuple giving the minimum and maximum x-axis
517
440
        data range.
518
441
        """
519
442
        try:
520
 
            lower = min([min(self._filter_none(s))
521
 
                         for type, s in self.annotated_data()
 
443
            lower = min([min(s) for type, s in self.annotated_data()
522
444
                         if type == 'x'])
523
 
            upper = max([max(self._filter_none(s))
524
 
                         for type, s in self.annotated_data()
 
445
            upper = max([max(s) for type, s in self.annotated_data()
525
446
                         if type == 'x'])
526
447
            return (lower, upper)
527
448
        except ValueError:
532
453
        data range.
533
454
        """
534
455
        try:
535
 
            lower = min([min(self._filter_none(s))
536
 
                         for type, s in self.annotated_data()
 
456
            lower = min([min(s) for type, s in self.annotated_data()
537
457
                         if type == 'y'])
538
 
            upper = max([max(self._filter_none(s)) + 1
539
 
                         for type, s in self.annotated_data()
 
458
            upper = max([max(s) for type, s in self.annotated_data()
540
459
                         if type == 'y'])
541
460
            return (lower, upper)
542
461
        except ValueError:
562
481
        if x_range is None:
563
482
            x_range = self.data_x_range()
564
483
            if x_range and x_range[0] > 0:
565
 
                x_range = (x_range[0], x_range[1])
 
484
                x_range = (0, x_range[1])
566
485
        self.scaled_x_range = x_range
567
486
 
568
487
        # Determine the y-axis range for scaling.
569
488
        if y_range is None:
570
489
            y_range = self.data_y_range()
571
490
            if y_range and y_range[0] > 0:
572
 
                y_range = (y_range[0], y_range[1])
 
491
                y_range = (0, y_range[1])
573
492
        self.scaled_y_range = y_range
574
493
 
575
494
        scaled_data = []
580
499
                scale_range = y_range
581
500
            elif type == 'marker-size':
582
501
                scale_range = (0, max(dataset))
583
 
            scaled_dataset = []
584
 
            for v in dataset:
585
 
                if v is None:
586
 
                    scaled_dataset.append(None)
587
 
                else:
588
 
                    scaled_dataset.append(
589
 
                        data_class.scale_value(v, scale_range))
590
 
            scaled_data.append(scaled_dataset)
 
502
            scaled_data.append([data_class.scale_value(v, scale_range)
 
503
                                for v in dataset])
591
504
        return scaled_data
592
505
 
593
506
    def add_data(self, data):
605
518
            data = self.data
606
519
        return repr(data_class(data))
607
520
 
608
 
    def annotated_data(self):
609
 
        for dataset in self.data:
610
 
            yield ('x', dataset)
611
 
 
612
521
    # Axis Labels
613
522
    # -------------------------------------------------------------------------
614
523
 
615
524
    def set_axis_labels(self, axis_type, values):
616
525
        assert(axis_type in Axis.TYPES)
617
 
        values = [urllib.quote(str(a)) for a in values]
 
526
        values = [ urllib.quote(a) for a in values ]
618
527
        axis_index = len(self.axis)
619
528
        axis = LabelAxis(axis_index, axis_type, values)
620
529
        self.axis.append(axis)
664
573
        url_bits = []
665
574
        url_bits.append('chxt=%s' % ','.join(available_axis))
666
575
        if label_axis:
667
 
            url_bits.append('chxl=%s' % '%7c'.join(label_axis))
 
576
            url_bits.append('chxl=%s' % '|'.join(label_axis))
668
577
        if range_axis:
669
 
            url_bits.append('chxr=%s' % '%7c'.join(range_axis))
 
578
            url_bits.append('chxr=%s' % '|'.join(range_axis))
670
579
        if positions:
671
 
            url_bits.append('chxp=%s' % '%7c'.join(positions))
 
580
            url_bits.append('chxp=%s' % '|'.join(positions))
672
581
        if styles:
673
 
            url_bits.append('chxs=%s' % '%7c'.join(styles))
 
582
            url_bits.append('chxs=%s' % '|'.join(styles))
674
583
        return '&'.join(url_bits)
675
584
 
676
585
    # Markers, Ranges and Fill area (chm)
677
586
    # -------------------------------------------------------------------------
678
587
 
679
 
    def markers_to_url(self):        
680
 
        return 'chm=%s' % '%7c'.join([','.join(a) for a in self.markers])
 
588
    def markers_to_url(self):
 
589
        return 'chm=%s' % '|'.join([','.join(a) for a in self.markers])
681
590
 
682
 
    def add_marker(self, index, point, marker_type, colour, size, priority=0):
 
591
    def add_marker(self, index, point, marker_type, colour, size):
683
592
        self.markers.append((marker_type, colour, str(index), str(point), \
684
 
            str(size), str(priority)))
 
593
            str(size)))
685
594
 
686
595
    def add_horizontal_range(self, colour, start, stop):
687
 
        self.markers.append(('r', colour, '0', str(start), str(stop)))
688
 
 
689
 
    def add_data_line(self, colour, data_set, size, priority=0):
690
 
        self.markers.append(('D', colour, str(data_set), '0', str(size), \
691
 
            str(priority)))
692
 
 
693
 
    def add_marker_text(self, string, colour, data_set, data_point, size, \
694
 
            priority=0):
695
 
        self.markers.append((str(string), colour, str(data_set), \
696
 
            str(data_point), str(size), str(priority)))        
 
596
        self.markers.append(('r', colour, '1', str(start), str(stop)))
697
597
 
698
598
    def add_vertical_range(self, colour, start, stop):
699
 
        self.markers.append(('R', colour, '0', str(start), str(stop)))
 
599
        self.markers.append(('R', colour, '1', str(start), str(stop)))
700
600
 
701
601
    def add_fill_range(self, colour, index_start, index_end):
702
602
        self.markers.append(('b', colour, str(index_start), str(index_end), \
705
605
    def add_fill_simple(self, colour):
706
606
        self.markers.append(('B', colour, '1', '1', '1'))
707
607
 
708
 
    # Line styles
709
 
    # -------------------------------------------------------------------------
 
608
 
 
609
class ScatterChart(Chart):
 
610
 
 
611
    def type_to_url(self):
 
612
        return 'cht=s'
 
613
 
 
614
    def annotated_data(self):
 
615
        yield ('x', self.data[0])
 
616
        yield ('y', self.data[1])
 
617
        if len(self.data) > 2:
 
618
            # The optional third dataset is relative sizing for point
 
619
            # markers.
 
620
            yield ('marker-size', self.data[2])
 
621
 
 
622
class LineChart(Chart):
 
623
 
 
624
    def __init__(self, *args, **kwargs):
 
625
        assert(type(self) != LineChart)  # This is an abstract class
 
626
        Chart.__init__(self, *args, **kwargs)
 
627
        self.line_styles = {}
 
628
        self.grid = None
710
629
 
711
630
    def set_line_style(self, index, thickness=1, line_segment=None, \
712
631
            blank_segment=None):
717
636
            value.append(str(blank_segment))
718
637
        self.line_styles[index] = value
719
638
 
720
 
    # Grid
721
 
    # -------------------------------------------------------------------------
722
 
 
723
639
    def set_grid(self, x_step, y_step, line_segment=1, \
724
640
            blank_segment=0):
725
641
        self.grid = '%s,%s,%s,%s' % (x_step, y_step, line_segment, \
726
642
            blank_segment)
727
643
 
728
 
 
729
 
class ScatterChart(Chart):
730
 
 
731
 
    def type_to_url(self):
732
 
        return 'cht=s'
733
 
 
734
 
    def annotated_data(self):
735
 
        yield ('x', self.data[0])
736
 
        yield ('y', self.data[1])
737
 
        if len(self.data) > 2:
738
 
            # The optional third dataset is relative sizing for point
739
 
            # markers.
740
 
            yield ('marker-size', self.data[2])
741
 
 
742
 
 
743
 
class LineChart(Chart):
744
 
 
745
 
    def __init__(self, *args, **kwargs):
746
 
        if type(self) == LineChart:
747
 
            raise AbstractClassException('This is an abstract class')
748
 
        Chart.__init__(self, *args, **kwargs)
 
644
    def get_url_bits(self):
 
645
        url_bits = Chart.get_url_bits(self)
 
646
        if self.line_styles:
 
647
            style = []
 
648
            # for index, values in self.line_style.items():
 
649
            for index in xrange(max(self.line_styles) + 1):
 
650
                if index in self.line_styles:
 
651
                    values = self.line_styles[index]
 
652
                else:
 
653
                    values = ('1', )
 
654
                style.append(','.join(values))
 
655
            url_bits.append('chls=%s' % '|'.join(style))
 
656
        if self.grid:
 
657
            url_bits.append('chg=%s' % self.grid)
 
658
        return url_bits
749
659
 
750
660
 
751
661
class SimpleLineChart(LineChart):
758
668
        for dataset in self.data:
759
669
            yield ('y', dataset)
760
670
 
761
 
 
762
671
class SparkLineChart(SimpleLineChart):
763
672
 
764
673
    def type_to_url(self):
765
674
        return 'cht=ls'
766
675
 
767
 
 
768
676
class XYLineChart(LineChart):
769
677
 
770
678
    def type_to_url(self):
778
686
            else:
779
687
                yield ('y', dataset)
780
688
 
781
 
 
782
689
class BarChart(Chart):
783
690
 
784
691
    def __init__(self, *args, **kwargs):
785
 
        if type(self) == BarChart:
786
 
            raise AbstractClassException('This is an abstract class')
 
692
        assert(type(self) != BarChart)  # This is an abstract class
787
693
        Chart.__init__(self, *args, **kwargs)
788
694
        self.bar_width = None
789
 
        self.zero_lines = {}
790
695
 
791
696
    def set_bar_width(self, bar_width):
792
697
        self.bar_width = bar_width
793
698
 
794
 
    def set_zero_line(self, index, zero_line):
795
 
        self.zero_lines[index] = zero_line
796
 
 
797
 
    def get_url_bits(self, data_class=None, skip_chbh=False):
798
 
        url_bits = Chart.get_url_bits(self, data_class=data_class)
799
 
        if not skip_chbh and self.bar_width is not None:
 
699
    def get_url_bits(self):
 
700
        url_bits = Chart.get_url_bits(self)
 
701
        if self.bar_width is not None:
800
702
            url_bits.append('chbh=%i' % self.bar_width)
801
 
        zero_line = []
802
 
        if self.zero_lines:
803
 
            for index in xrange(max(self.zero_lines) + 1):
804
 
                if index in self.zero_lines:
805
 
                    zero_line.append(str(self.zero_lines[index]))
806
 
                else:
807
 
                    zero_line.append('0')
808
 
            url_bits.append('chp=%s' % ','.join(zero_line))
809
703
        return url_bits
810
704
 
811
705
 
814
708
    def type_to_url(self):
815
709
        return 'cht=bhs'
816
710
 
 
711
    def annotated_data(self):
 
712
        for dataset in self.data:
 
713
            yield ('x', dataset)
817
714
 
818
715
class StackedVerticalBarChart(BarChart):
819
716
 
828
725
class GroupedBarChart(BarChart):
829
726
 
830
727
    def __init__(self, *args, **kwargs):
831
 
        if type(self) == GroupedBarChart:
832
 
            raise AbstractClassException('This is an abstract class')
 
728
        assert(type(self) != GroupedBarChart)  # This is an abstract class
833
729
        BarChart.__init__(self, *args, **kwargs)
834
730
        self.bar_spacing = None
835
731
        self.group_spacing = None
842
738
        """Set spacing between groups of bars."""
843
739
        self.group_spacing = spacing
844
740
 
845
 
    def get_url_bits(self, data_class=None):
 
741
    def get_url_bits(self):
846
742
        # Skip 'BarChart.get_url_bits' and call Chart directly so the parent
847
743
        # doesn't add "chbh" before we do.
848
 
        url_bits = BarChart.get_url_bits(self, data_class=data_class,
849
 
            skip_chbh=True)
 
744
        url_bits = Chart.get_url_bits(self)
850
745
        if self.group_spacing is not None:
851
746
            if self.bar_spacing is None:
852
 
                raise InvalidParametersException('Bar spacing is required ' \
853
 
                    'to be set when setting group spacing')
 
747
                raise InvalidParametersException('Bar spacing is required to ' \
 
748
                    'be set when setting group spacing')
854
749
            if self.bar_width is None:
855
750
                raise InvalidParametersException('Bar width is required to ' \
856
751
                    'be set when setting bar spacing')
861
756
                raise InvalidParametersException('Bar width is required to ' \
862
757
                    'be set when setting bar spacing')
863
758
            url_bits.append('chbh=%i,%i' % (self.bar_width, self.bar_spacing))
864
 
        elif self.bar_width:
 
759
        else:
865
760
            url_bits.append('chbh=%i' % self.bar_width)
866
761
        return url_bits
867
762
 
871
766
    def type_to_url(self):
872
767
        return 'cht=bhg'
873
768
 
 
769
    def annotated_data(self):
 
770
        for dataset in self.data:
 
771
            yield ('x', dataset)
 
772
 
874
773
 
875
774
class GroupedVerticalBarChart(GroupedBarChart):
876
775
 
885
784
class PieChart(Chart):
886
785
 
887
786
    def __init__(self, *args, **kwargs):
888
 
        if type(self) == PieChart:
889
 
            raise AbstractClassException('This is an abstract class')
 
787
        assert(type(self) != PieChart)  # This is an abstract class
890
788
        Chart.__init__(self, *args, **kwargs)
891
789
        self.pie_labels = []
892
 
        if self.y_range:
893
 
            warnings.warn('y_range is not used with %s.' % \
894
 
                (self.__class__.__name__))
895
790
 
896
791
    def set_pie_labels(self, labels):
897
792
        self.pie_labels = [urllib.quote(a) for a in labels]
898
793
 
899
 
    def get_url_bits(self, data_class=None):
900
 
        url_bits = Chart.get_url_bits(self, data_class=data_class)
 
794
    def get_url_bits(self):
 
795
        url_bits = Chart.get_url_bits(self)
901
796
        if self.pie_labels:
902
 
            url_bits.append('chl=%s' % '%7c'.join(self.pie_labels))
 
797
            url_bits.append('chl=%s' % '|'.join(self.pie_labels))
903
798
        return url_bits
904
799
 
905
800
    def annotated_data(self):
906
801
        # Datasets are all y-axis data. However, there should only be
907
802
        # one dataset for pie charts.
908
803
        for dataset in self.data:
909
 
            yield ('x', dataset)
910
 
 
911
 
    def scaled_data(self, data_class, x_range=None, y_range=None):
912
 
        if not x_range:
913
 
            x_range = [0, sum(self.data[0])]
914
 
        return Chart.scaled_data(self, data_class, x_range, self.y_range)
 
804
            yield ('y', dataset)
915
805
 
916
806
 
917
807
class PieChart2D(PieChart):
936
826
            yield ('y', dataset)
937
827
 
938
828
 
939
 
class RadarChart(Chart):
940
 
 
941
 
    def type_to_url(self):
942
 
        return 'cht=r'
943
 
 
944
 
 
945
 
class SplineRadarChart(RadarChart):
946
 
 
947
 
    def type_to_url(self):
948
 
        return 'cht=rs'
949
 
 
950
 
 
951
 
class MapChart(Chart):
952
 
 
953
 
    def __init__(self, *args, **kwargs):
954
 
        Chart.__init__(self, *args, **kwargs)
955
 
        self.geo_area = 'world'
956
 
        self.codes = []
957
 
 
958
 
    def type_to_url(self):
959
 
        return 'cht=t'
960
 
 
961
 
    def set_codes(self, codes):
962
 
        self.codes = codes
963
 
 
964
 
    def get_url_bits(self, data_class=None):
965
 
        url_bits = Chart.get_url_bits(self, data_class=data_class)
966
 
        url_bits.append('chtm=%s' % self.geo_area)
967
 
        if self.codes:
968
 
            url_bits.append('chld=%s' % ''.join(self.codes))
969
 
        return url_bits
970
 
 
971
 
 
972
 
class GoogleOMeterChart(PieChart):
973
 
    """Inheriting from PieChart because of similar labeling"""
974
 
 
975
 
    def __init__(self, *args, **kwargs):
976
 
        PieChart.__init__(self, *args, **kwargs)
977
 
        if self.auto_scale and not self.x_range:
978
 
            warnings.warn('Please specify an x_range with GoogleOMeterChart, '
979
 
                'otherwise one arrow will always be at the max.')
980
 
 
981
 
    def type_to_url(self):
982
 
        return 'cht=gom'
983
 
 
984
 
 
985
 
class QRChart(Chart):
986
 
 
987
 
    def __init__(self, *args, **kwargs):
988
 
        Chart.__init__(self, *args, **kwargs)
989
 
        self.encoding = None
990
 
        self.ec_level = None
991
 
        self.margin = None
992
 
 
993
 
    def type_to_url(self):
994
 
        return 'cht=qr'
995
 
 
996
 
    def data_to_url(self, data_class=None):
997
 
        if not self.data:
998
 
            raise NoDataGivenException()
999
 
        return 'chl=%s' % urllib.quote(self.data[0])
1000
 
 
1001
 
    def get_url_bits(self, data_class=None):
1002
 
        url_bits = Chart.get_url_bits(self, data_class=data_class)
1003
 
        if self.encoding:
1004
 
            url_bits.append('choe=%s' % self.encoding)
1005
 
        if self.ec_level:
1006
 
            url_bits.append('chld=%s%%7c%s' % (self.ec_level, self.margin))
1007
 
        return url_bits
1008
 
 
1009
 
    def set_encoding(self, encoding):
1010
 
        self.encoding = encoding
1011
 
 
1012
 
    def set_ec(self, level, margin):
1013
 
        self.ec_level = level
1014
 
        self.margin = margin
1015
 
 
1016
 
 
1017
 
class ChartGrammar(object):
1018
 
 
1019
 
    def __init__(self):
1020
 
        self.grammar = None
1021
 
        self.chart = None
1022
 
 
1023
 
    def parse(self, grammar):
1024
 
        self.grammar = grammar
1025
 
        self.chart = self.create_chart_instance()
1026
 
 
1027
 
        for attr in self.grammar:
1028
 
            if attr in ('w', 'h', 'type', 'auto_scale', 'x_range', 'y_range'):
1029
 
                continue  # These are already parsed in create_chart_instance
1030
 
            attr_func = 'parse_' + attr
1031
 
            if not hasattr(self, attr_func):
1032
 
                warnings.warn('No parser for grammar attribute "%s"' % (attr))
1033
 
                continue
1034
 
            getattr(self, attr_func)(grammar[attr])
1035
 
 
1036
 
        return self.chart
1037
 
 
1038
 
    def parse_data(self, data):
1039
 
        self.chart.data = data
1040
 
 
1041
 
    @staticmethod
1042
 
    def get_possible_chart_types():
1043
 
        possible_charts = []
1044
 
        for cls_name in globals().keys():
1045
 
            if not cls_name.endswith('Chart'):
1046
 
                continue
1047
 
            cls = globals()[cls_name]
1048
 
            # Check if it is an abstract class
1049
 
            try:
1050
 
                a = cls(1, 1, auto_scale=False)
1051
 
                del a
1052
 
            except AbstractClassException:
1053
 
                continue
1054
 
            # Strip off "Class"
1055
 
            possible_charts.append(cls_name[:-5])
1056
 
        return possible_charts
1057
 
 
1058
 
    def create_chart_instance(self, grammar=None):
1059
 
        if not grammar:
1060
 
            grammar = self.grammar
1061
 
        assert(isinstance(grammar, dict))  # grammar must be a dict
1062
 
        assert('w' in grammar)  # width is required
1063
 
        assert('h' in grammar)  # height is required
1064
 
        assert('type' in grammar)  # type is required
1065
 
        chart_type = grammar['type']
1066
 
        w = grammar['w']
1067
 
        h = grammar['h']
1068
 
        auto_scale = grammar.get('auto_scale', None)
1069
 
        x_range = grammar.get('x_range', None)
1070
 
        y_range = grammar.get('y_range', None)
1071
 
        types = ChartGrammar.get_possible_chart_types()
1072
 
        if chart_type not in types:
1073
 
            raise UnknownChartType('%s is an unknown chart type. Possible '
1074
 
                'chart types are %s' % (chart_type, ','.join(types)))
1075
 
        return globals()[chart_type + 'Chart'](w, h, auto_scale=auto_scale,
1076
 
            x_range=x_range, y_range=y_range)
1077
 
 
1078
 
    def download(self):
1079
 
        pass
1080
 
 
 
829
def test():
 
830
    chart = GroupedVerticalBarChart(320, 200)
 
831
    chart = PieChart2D(320, 200)
 
832
    chart = ScatterChart(320, 200)
 
833
    chart = SimpleLineChart(320, 200)
 
834
    sine_data = [math.sin(float(a) / 10) * 2000 + 2000 for a in xrange(100)]
 
835
    random_data = [a * random.random() * 30 for a in xrange(40)]
 
836
    random_data2 = [random.random() * 4000 for a in xrange(10)]
 
837
#    chart.set_bar_width(50)
 
838
#    chart.set_bar_spacing(0)
 
839
    chart.add_data(sine_data)
 
840
    chart.add_data(random_data)
 
841
    chart.add_data(random_data2)
 
842
#    chart.set_line_style(1, thickness=2)
 
843
#    chart.set_line_style(2, line_segment=10, blank_segment=5)
 
844
#    chart.set_title('heloooo')
 
845
#    chart.set_legend(('sine wave', 'random * x'))
 
846
#    chart.set_colours(('ee2000', 'DDDDAA', 'fF03f2'))
 
847
#    chart.fill_solid(Chart.BACKGROUND, '123456')
 
848
#    chart.fill_linear_gradient(Chart.CHART, 20, '004070', 1, '300040', 0,
 
849
#        'aabbcc00', 0.5)
 
850
#    chart.fill_linear_stripes(Chart.CHART, 20, '204070', .2, '300040', .2,
 
851
#        'aabbcc00', 0.2)
 
852
    axis_left_index = chart.set_axis_range(Axis.LEFT, 0, 10)
 
853
    axis_left_index = chart.set_axis_range(Axis.LEFT, 0, 10)
 
854
    axis_left_index = chart.set_axis_range(Axis.LEFT, 0, 10)
 
855
    axis_right_index = chart.set_axis_range(Axis.RIGHT, 5, 30)
 
856
    axis_bottom_index = chart.set_axis_labels(Axis.BOTTOM, [1, 25, 95])
 
857
    chart.set_axis_positions(axis_bottom_index, [1, 25, 95])
 
858
    chart.set_axis_style(axis_bottom_index, '003050', 15)
 
859
 
 
860
#    chart.set_pie_labels(('apples', 'oranges', 'bananas'))
 
861
 
 
862
#    chart.set_grid(10, 10)
 
863
 
 
864
#    for a in xrange(0, 100, 10):
 
865
#        chart.add_marker(1, a, 'a', 'AACA20', 10)
 
866
 
 
867
    chart.add_horizontal_range('00A020', .2, .5)
 
868
    chart.add_vertical_range('00c030', .2, .4)
 
869
 
 
870
    chart.add_fill_simple('303030A0')
 
871
 
 
872
    chart.download('test.png')
 
873
 
 
874
    url = chart.get_url()
 
875
    print url
 
876
    if 0:
 
877
        data = urllib.urlopen(chart.get_url()).read()
 
878
        open('meh.png', 'wb').write(data)
 
879
        os.system('start meh.png')
 
880
 
 
881
 
 
882
if __name__ == '__main__':
 
883
    test()