/+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: Gustav Hartvigsson
  • Date: 2011-01-03 21:57:12 UTC
  • Revision ID: gustav.hartvigsson@gmail.com-20110103215712-1yeiw9tl7oiwh8w1
forgot the the the images in the examples folder...

Show diffs side-by-side

added added

removed removed

Lines of Context:
3
3
 
4
4
http://pygooglechart.slowchop.com/
5
5
 
6
 
Copyright 2007-2008 Gerald Kaszuba
 
6
Copyright 2007-2009 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
22
23
 
23
24
import os
24
25
import urllib
25
 
import urllib2
 
26
import urllib.request, urllib.error
26
27
import math
27
28
import random
28
29
import re
32
33
# Helper variables and functions
33
34
# -----------------------------------------------------------------------------
34
35
 
35
 
__version__ = '0.2.1'
 
36
__version__ = '0.3.0'
36
37
__author__ = 'Gerald Kaszuba'
37
38
 
38
39
reo_colour = re.compile('^([A-Fa-f0-9]{2,2}){3,4}$')
47
48
def _reset_warnings():
48
49
    """Helper function to reset all warnings. Used by the unit tests."""
49
50
    globals()['__warningregistry__'] = None
50
 
#def _warn(message):
51
 
#    warnings.warn_explicit(msg, warnings.UserWarning,
52
51
 
53
52
 
54
53
# Exception Classes
86
85
class UnknownChartType(PyGoogleChartException):
87
86
    pass
88
87
 
 
88
class UnknownCountryCodeException(PyGoogleChartException):
 
89
    pass
89
90
 
90
91
# Data Classes
91
92
# -----------------------------------------------------------------------------
102
103
    def float_scale_value(cls, value, range):
103
104
        lower, upper = range
104
105
        assert(upper > lower)
105
 
        scaled = (value - lower) * (float(cls.max_value) / (upper - lower))
 
106
        scaled = (value - lower) * (cls.max_value / (upper - lower))
106
107
        return scaled
107
108
 
108
109
    @classmethod
117
118
    def scale_value(cls, value, range):
118
119
        scaled = cls.int_scale_value(value, range)
119
120
        clipped = cls.clip_value(scaled)
 
121
        Data.check_clip(scaled, clipped)
 
122
        return clipped
 
123
 
 
124
    @staticmethod
 
125
    def check_clip(scaled, clipped):
120
126
        if clipped != scaled:
121
127
            warnings.warn('One or more of of your data points has been '
122
128
                'clipped because it is out of range.')
123
 
        return clipped
124
129
 
125
130
 
126
131
class SimpleData(Data):
160
165
                else:
161
166
                    raise DataOutOfRangeException()
162
167
            encoded_data.append(','.join(sub_data))
163
 
        return 'chd=t:' + '|'.join(encoded_data)
 
168
        return 'chd=t:' + '%7c'.join(encoded_data)
164
169
 
165
170
    @classmethod
166
171
    def scale_value(cls, value, range):
168
173
        # map index
169
174
        scaled = cls.float_scale_value(value, range)
170
175
        clipped = cls.clip_value(scaled)
 
176
        Data.check_clip(scaled, clipped)
171
177
        return clipped
172
178
 
173
179
 
254
260
        self.values = [str(a) for a in values]
255
261
 
256
262
    def __repr__(self):
257
 
        return '%i:|%s' % (self.axis_index, '|'.join(self.values))
 
263
        return '%i:%%7c%s' % (self.axis_index, '%7c'.join(self.values))
258
264
 
259
265
 
260
266
class RangeAxis(Axis):
278
284
    of the chart. legend requires a list that corresponds to datasets.
279
285
    """
280
286
 
281
 
    BASE_URL = 'http://chart.apis.google.com/chart?'
 
287
    BASE_URL = 'http://chart.apis.google.com/chart'
282
288
    BACKGROUND = 'bg'
283
289
    CHART = 'c'
284
290
    ALPHA = 'a'
288
294
    LINEAR_STRIPES = 'ls'
289
295
 
290
296
    def __init__(self, width, height, title=None, legend=None, colours=None,
291
 
            auto_scale=True, x_range=None, y_range=None):
 
297
            auto_scale=True, x_range=None, y_range=None,
 
298
            colours_within_series=None):
292
299
        if type(self) == Chart:
293
300
            raise AbstractClassException('This is an abstract class')
294
301
        assert(isinstance(width, int))
297
304
        self.height = height
298
305
        self.data = []
299
306
        self.set_title(title)
 
307
        self.set_title_style(None, None)
300
308
        self.set_legend(legend)
 
309
        self.set_legend_position(None)
301
310
        self.set_colours(colours)
 
311
        self.set_colours_within_series(colours_within_series)
302
312
 
303
313
        # Data for scaling.
304
314
        self.auto_scale = auto_scale  # Whether to automatically scale data
322
332
        self.markers = []
323
333
        self.line_styles = {}
324
334
        self.grid = None
 
335
        self.title_colour = None
 
336
        self.title_font_size = None
325
337
 
326
338
    # URL generation
327
339
    # -------------------------------------------------------------------------
328
 
 
 
340
        
329
341
    def get_url(self, data_class=None):
 
342
        return self.BASE_URL + '?' + self.get_url_extension(data_class)
 
343
    
 
344
    def get_url_extension(self, data_class=None):
330
345
        url_bits = self.get_url_bits(data_class=data_class)
331
 
        return self.BASE_URL + '&'.join(url_bits)
 
346
        return '&'.join(url_bits)
332
347
 
333
348
    def get_url_bits(self, data_class=None):
334
349
        url_bits = []
339
354
        # optional arguments
340
355
        if self.title:
341
356
            url_bits.append('chtt=%s' % self.title)
 
357
        if self.title_colour and self.title_font_size:
 
358
            url_bits.append('chts=%s,%s' % (self.title_colour, \
 
359
                self.title_font_size))
342
360
        if self.legend:
343
 
            url_bits.append('chdl=%s' % '|'.join(self.legend))
 
361
            url_bits.append('chdl=%s' % '%7c'.join(self.legend))
 
362
        if self.legend_position:
 
363
            url_bits.append('chdlp=%s' % (self.legend_position))
344
364
        if self.colours:
345
 
            url_bits.append('chco=%s' % ','.join(self.colours))
 
365
            url_bits.append('chco=%s' % ','.join(self.colours))            
 
366
        if self.colours_within_series:
 
367
            url_bits.append('chco=%s' % '%7c'.join(self.colours_within_series))
346
368
        ret = self.fill_to_url()
347
369
        if ret:
348
370
            url_bits.append(ret)
349
371
        ret = self.axis_to_url()
350
372
        if ret:
351
 
            url_bits.append(ret)
 
373
            url_bits.append(ret)                    
352
374
        if self.markers:
353
 
            url_bits.append(self.markers_to_url())
 
375
            url_bits.append(self.markers_to_url())        
354
376
        if self.line_styles:
355
377
            style = []
356
 
            for index in xrange(max(self.line_styles) + 1):
 
378
            for index in range(max(self.line_styles) + 1):
357
379
                if index in self.line_styles:
358
380
                    values = self.line_styles[index]
359
381
                else:
360
382
                    values = ('1', )
361
383
                style.append(','.join(values))
362
 
            url_bits.append('chls=%s' % '|'.join(style))
 
384
            url_bits.append('chls=%s' % '%7c'.join(style))
363
385
        if self.grid:
364
386
            url_bits.append('chg=%s' % self.grid)
365
387
        return url_bits
367
389
    # Downloading
368
390
    # -------------------------------------------------------------------------
369
391
 
370
 
    def download(self, file_name):
371
 
        opener = urllib2.urlopen(self.get_url())
 
392
    def download(self, file_name, use_post=True):
 
393
        if use_post:
 
394
            opener = urllib.request.urlopen(self.BASE_URL, self.get_url_extension())
 
395
        else:
 
396
            opener = urllib.request.urlopen(self.get_url())
372
397
 
373
398
        if opener.headers['content-type'] != 'image/png':
374
399
            raise BadContentTypeException('Server responded with a ' \
375
400
                'content-type of %s' % opener.headers['content-type'])
376
401
 
377
 
        open(file_name, 'wb').write(urllib.urlopen(self.get_url()).read())
 
402
        open(file_name, 'wb').write(opener.read())
378
403
 
379
404
    # Simple settings
380
405
    # -------------------------------------------------------------------------
381
406
 
382
407
    def set_title(self, title):
383
408
        if title:
384
 
            self.title = urllib.quote(title)
 
409
            self.title = urllib.parse.quote(title)
385
410
        else:
386
411
            self.title = None
387
412
 
 
413
    def set_title_style(self, colour=None, font_size=None):
 
414
        if not colour is None:
 
415
            _check_colour(colour)
 
416
        if not colour and not font_size:
 
417
            return
 
418
        self.title_colour = colour or '333333'
 
419
        self.title_font_size = font_size or 13.5
 
420
 
388
421
    def set_legend(self, legend):
389
422
        """legend needs to be a list, tuple or None"""
390
423
        assert(isinstance(legend, list) or isinstance(legend, tuple) or
391
424
            legend is None)
392
425
        if legend:
393
 
            self.legend = [urllib.quote(a) for a in legend]
 
426
            self.legend = [urllib.parse.quote(a) for a in legend]
394
427
        else:
395
428
            self.legend = None
396
429
 
 
430
    def set_legend_position(self, legend_position):
 
431
        if legend_position:
 
432
            self.legend_position = urllib.parse.quote(legend_position)
 
433
        else:    
 
434
            self.legend_position = None
 
435
 
397
436
    # Chart colours
398
437
    # -------------------------------------------------------------------------
399
438
 
407
446
                _check_colour(col)
408
447
        self.colours = colours
409
448
 
 
449
    def set_colours_within_series(self, colours):
 
450
        # colours needs to be a list, tuple or None
 
451
        assert(isinstance(colours, list) or isinstance(colours, tuple) or
 
452
            colours is None)
 
453
        # make sure the colours are in the right format
 
454
        if colours:
 
455
            for col in colours:
 
456
                _check_colour(col)
 
457
        self.colours_within_series = colours        
 
458
 
410
459
    # Background/Chart colours
411
460
    # -------------------------------------------------------------------------
412
461
 
421
470
        assert(angle >= 0 and angle <= 90)
422
471
        assert(len(args) % 2 == 0)
423
472
        args = list(args)  # args is probably a tuple and we need to mutate
424
 
        for a in xrange(len(args) / 2):
 
473
        for a in range(int(len(args) / 2)):
425
474
            col = args[a * 2]
426
475
            offset = args[a * 2 + 1]
427
476
            _check_colour(col)
448
497
                areas.append('%s,%s,%s' % (area, self.fill_types[area], \
449
498
                    self.fill_area[area]))
450
499
        if areas:
451
 
            return 'chf=' + '|'.join(areas)
 
500
            return 'chf=' + '%7c'.join(areas)
452
501
 
453
502
    # Data
454
503
    # -------------------------------------------------------------------------
472
521
        else:
473
522
            return ExtendedData
474
523
 
 
524
    def _filter_none(self, data):
 
525
        return [r for r in data if r is not None]
 
526
 
475
527
    def data_x_range(self):
476
528
        """Return a 2-tuple giving the minimum and maximum x-axis
477
529
        data range.
478
530
        """
479
531
        try:
480
 
            lower = min([min(s) for type, s in self.annotated_data()
 
532
            lower = min([min(self._filter_none(s))
 
533
                         for type, s in self.annotated_data()
481
534
                         if type == 'x'])
482
 
            upper = max([max(s) for type, s in self.annotated_data()
 
535
            upper = max([max(self._filter_none(s))
 
536
                         for type, s in self.annotated_data()
483
537
                         if type == 'x'])
484
538
            return (lower, upper)
485
539
        except ValueError:
490
544
        data range.
491
545
        """
492
546
        try:
493
 
            lower = min([min(s) for type, s in self.annotated_data()
 
547
            lower = min([min(self._filter_none(s))
 
548
                         for type, s in self.annotated_data()
494
549
                         if type == 'y'])
495
 
            upper = max([max(s) + 1 for type, s in self.annotated_data()
 
550
            upper = max([max(self._filter_none(s)) + 1
 
551
                         for type, s in self.annotated_data()
496
552
                         if type == 'y'])
497
553
            return (lower, upper)
498
554
        except ValueError:
518
574
        if x_range is None:
519
575
            x_range = self.data_x_range()
520
576
            if x_range and x_range[0] > 0:
521
 
                x_range = (0, x_range[1])
 
577
                x_range = (x_range[0], x_range[1])
522
578
        self.scaled_x_range = x_range
523
579
 
524
580
        # Determine the y-axis range for scaling.
525
581
        if y_range is None:
526
582
            y_range = self.data_y_range()
527
583
            if y_range and y_range[0] > 0:
528
 
                y_range = (0, y_range[1])
 
584
                y_range = (y_range[0], y_range[1])
529
585
        self.scaled_y_range = y_range
530
586
 
531
587
        scaled_data = []
536
592
                scale_range = y_range
537
593
            elif type == 'marker-size':
538
594
                scale_range = (0, max(dataset))
539
 
            scaled_data.append([data_class.scale_value(v, scale_range)
540
 
                                for v in dataset])
 
595
            scaled_dataset = []
 
596
            for v in dataset:
 
597
                if v is None:
 
598
                    scaled_dataset.append(None)
 
599
                else:
 
600
                    scaled_dataset.append(
 
601
                        data_class.scale_value(v, scale_range))
 
602
            scaled_data.append(scaled_dataset)
541
603
        return scaled_data
542
604
 
543
605
    def add_data(self, data):
564
626
 
565
627
    def set_axis_labels(self, axis_type, values):
566
628
        assert(axis_type in Axis.TYPES)
567
 
        values = [urllib.quote(a) for a in values]
 
629
        values = [urllib.parse.quote(str(a)) for a in values]
568
630
        axis_index = len(self.axis)
569
631
        axis = LabelAxis(axis_index, axis_type, values)
570
632
        self.axis.append(axis)
614
676
        url_bits = []
615
677
        url_bits.append('chxt=%s' % ','.join(available_axis))
616
678
        if label_axis:
617
 
            url_bits.append('chxl=%s' % '|'.join(label_axis))
 
679
            url_bits.append('chxl=%s' % '%7c'.join(label_axis))
618
680
        if range_axis:
619
 
            url_bits.append('chxr=%s' % '|'.join(range_axis))
 
681
            url_bits.append('chxr=%s' % '%7c'.join(range_axis))
620
682
        if positions:
621
 
            url_bits.append('chxp=%s' % '|'.join(positions))
 
683
            url_bits.append('chxp=%s' % '%7c'.join(positions))
622
684
        if styles:
623
 
            url_bits.append('chxs=%s' % '|'.join(styles))
 
685
            url_bits.append('chxs=%s' % '%7c'.join(styles))
624
686
        return '&'.join(url_bits)
625
687
 
626
688
    # Markers, Ranges and Fill area (chm)
627
689
    # -------------------------------------------------------------------------
628
690
 
629
 
    def markers_to_url(self):
630
 
        return 'chm=%s' % '|'.join([','.join(a) for a in self.markers])
 
691
    def markers_to_url(self):        
 
692
        return 'chm=%s' % '%7c'.join([','.join(a) for a in self.markers])
631
693
 
632
694
    def add_marker(self, index, point, marker_type, colour, size, priority=0):
633
695
        self.markers.append((marker_type, colour, str(index), str(point), \
634
696
            str(size), str(priority)))
635
697
 
636
698
    def add_horizontal_range(self, colour, start, stop):
637
 
        self.markers.append(('r', colour, '1', str(start), str(stop)))
 
699
        self.markers.append(('r', colour, '0', str(start), str(stop)))
 
700
 
 
701
    def add_data_line(self, colour, data_set, size, priority=0):
 
702
        self.markers.append(('D', colour, str(data_set), '0', str(size), \
 
703
            str(priority)))
 
704
 
 
705
    def add_marker_text(self, string, colour, data_set, data_point, size, \
 
706
            priority=0):
 
707
        self.markers.append((str(string), colour, str(data_set), \
 
708
            str(data_point), str(size), str(priority)))        
638
709
 
639
710
    def add_vertical_range(self, colour, start, stop):
640
 
        self.markers.append(('R', colour, '1', str(start), str(stop)))
 
711
        self.markers.append(('R', colour, '0', str(start), str(stop)))
641
712
 
642
713
    def add_fill_range(self, colour, index_start, index_end):
643
714
        self.markers.append(('b', colour, str(index_start), str(index_end), \
741
812
            url_bits.append('chbh=%i' % self.bar_width)
742
813
        zero_line = []
743
814
        if self.zero_lines:
744
 
            for index in xrange(max(self.zero_lines) + 1):
 
815
            for index in range(max(self.zero_lines) + 1):
745
816
                if index in self.zero_lines:
746
817
                    zero_line.append(str(self.zero_lines[index]))
747
818
                else:
835
906
                (self.__class__.__name__))
836
907
 
837
908
    def set_pie_labels(self, labels):
838
 
        self.pie_labels = [urllib.quote(a) for a in labels]
 
909
        self.pie_labels = [urllib.parse.quote(a) for a in labels]
839
910
 
840
911
    def get_url_bits(self, data_class=None):
841
912
        url_bits = Chart.get_url_bits(self, data_class=data_class)
842
913
        if self.pie_labels:
843
 
            url_bits.append('chl=%s' % '|'.join(self.pie_labels))
 
914
            url_bits.append('chl=%s' % '%7c'.join(self.pie_labels))
844
915
        return url_bits
845
916
 
846
917
    def annotated_data(self):
849
920
        for dataset in self.data:
850
921
            yield ('x', dataset)
851
922
 
 
923
    def scaled_data(self, data_class, x_range=None, y_range=None):
 
924
        if not x_range:
 
925
            x_range = [0, sum(self.data[0])]
 
926
        return Chart.scaled_data(self, data_class, x_range, self.y_range)
 
927
 
852
928
 
853
929
class PieChart2D(PieChart):
854
930
 
890
966
        Chart.__init__(self, *args, **kwargs)
891
967
        self.geo_area = 'world'
892
968
        self.codes = []
893
 
 
 
969
        self.__areas = ('africa', 'asia', 'europe', 'middle_east',
 
970
            'south_america', 'usa', 'world')
 
971
        self.__ccodes = (
 
972
            'AD', 'AE', 'AF', 'AG', 'AI', 'AL', 'AM', 'AN', 'AO', 'AQ', 'AR',
 
973
            'AS', 'AT', 'AU', 'AW', 'AX', 'AZ', 'BA', 'BB', 'BD', 'BE', 'BF',
 
974
            'BG', 'BH', 'BI', 'BJ', 'BL', 'BM', 'BN', 'BO', 'BR', 'BS', 'BT',
 
975
            'BV', 'BW', 'BY', 'BZ', 'CA', 'CC', 'CD', 'CF', 'CG', 'CH', 'CI',
 
976
            'CK', 'CL', 'CM', 'CN', 'CO', 'CR', 'CU', 'CV', 'CX', 'CY', 'CZ',
 
977
            'DE', 'DJ', 'DK', 'DM', 'DO', 'DZ', 'EC', 'EE', 'EG', 'EH', 'ER',
 
978
            'ES', 'ET', 'FI', 'FJ', 'FK', 'FM', 'FO', 'FR', 'GA', 'GB', 'GD',
 
979
            'GE', 'GF', 'GG', 'GH', 'GI', 'GL', 'GM', 'GN', 'GP', 'GQ', 'GR',
 
980
            'GS', 'GT', 'GU', 'GW', 'GY', 'HK', 'HM', 'HN', 'HR', 'HT', 'HU',
 
981
            'ID', 'IE', 'IL', 'IM', 'IN', 'IO', 'IQ', 'IR', 'IS', 'IT', 'JE',
 
982
            'JM', 'JO', 'JP', 'KE', 'KG', 'KH', 'KI', 'KM', 'KN', 'KP', 'KR',
 
983
            'KW', 'KY', 'KZ', 'LA', 'LB', 'LC', 'LI', 'LK', 'LR', 'LS', 'LT',
 
984
            'LU', 'LV', 'LY', 'MA', 'MC', 'MD', 'ME', 'MF', 'MG', 'MH', 'MK',
 
985
            'ML', 'MM', 'MN', 'MO', 'MP', 'MQ', 'MR', 'MS', 'MT', 'MU', 'MV',
 
986
            'MW', 'MX', 'MY', 'MZ', 'NA', 'NC', 'NE', 'NF', 'NG', 'NI', 'NL',
 
987
            'NO', 'NP', 'NR', 'NU', 'NZ', 'OM', 'PA', 'PE', 'PF', 'PG', 'PH',
 
988
            'PK', 'PL', 'PM', 'PN', 'PR', 'PS', 'PT', 'PW', 'PY', 'QA', 'RE',
 
989
            'RO', 'RS', 'RU', 'RW', 'SA', 'SB', 'SC', 'SD', 'SE', 'SG', 'SH',
 
990
            'SI', 'SJ', 'SK', 'SL', 'SM', 'SN', 'SO', 'SR', 'ST', 'SV', 'SY',
 
991
            'SZ', 'TC', 'TD', 'TF', 'TG', 'TH', 'TJ', 'TK', 'TL', 'TM', 'TN',
 
992
            'TO', 'TR', 'TT', 'TV', 'TW', 'TZ', 'UA', 'UG', 'UM', 'US', 'UY',
 
993
            'UZ', 'VA', 'VC', 'VE', 'VG', 'VI', 'VN', 'VU', 'WF', 'WS', 'YE',
 
994
            'YT', 'ZA', 'ZM', 'ZW')
 
995
        
894
996
    def type_to_url(self):
895
997
        return 'cht=t'
896
998
 
897
999
    def set_codes(self, codes):
898
 
        self.codes = codes
 
1000
        '''Set the country code map for the data.
 
1001
        Codes given in a list.
 
1002
 
 
1003
        i.e. DE - Germany
 
1004
             AT - Austria
 
1005
             US - United States
 
1006
        '''
 
1007
 
 
1008
        codemap = ''
 
1009
        
 
1010
        for cc in codes:
 
1011
            cc = cc.upper()
 
1012
            if cc in self.__ccodes:
 
1013
                codemap += cc
 
1014
            else:
 
1015
                raise UnknownCountryCodeException(cc)
 
1016
            
 
1017
        self.codes = codemap
 
1018
 
 
1019
    def set_geo_area(self, area):
 
1020
        '''Sets the geo area for the map.
 
1021
 
 
1022
        * africa
 
1023
        * asia
 
1024
        * europe
 
1025
        * middle_east
 
1026
        * south_america
 
1027
        * usa
 
1028
        * world
 
1029
        '''
 
1030
        
 
1031
        if area in self.__areas:
 
1032
            self.geo_area = area
 
1033
        else:
 
1034
            raise UnknownChartType('Unknown chart type for maps: %s' %area)
899
1035
 
900
1036
    def get_url_bits(self, data_class=None):
901
1037
        url_bits = Chart.get_url_bits(self, data_class=data_class)
904
1040
            url_bits.append('chld=%s' % ''.join(self.codes))
905
1041
        return url_bits
906
1042
 
 
1043
    def add_data_dict(self, datadict):
 
1044
        '''Sets the data and country codes via a dictionary.
 
1045
 
 
1046
        i.e. {'DE': 50, 'GB': 30, 'AT': 70}
 
1047
        '''
 
1048
 
 
1049
        self.set_codes(datadict.keys())
 
1050
        self.add_data(datadict.values())
 
1051
 
907
1052
 
908
1053
class GoogleOMeterChart(PieChart):
909
1054
    """Inheriting from PieChart because of similar labeling"""
918
1063
        return 'cht=gom'
919
1064
 
920
1065
 
 
1066
class QRChart(Chart):
 
1067
 
 
1068
    def __init__(self, *args, **kwargs):
 
1069
        Chart.__init__(self, *args, **kwargs)
 
1070
        self.encoding = None
 
1071
        self.ec_level = None
 
1072
        self.margin = None
 
1073
 
 
1074
    def type_to_url(self):
 
1075
        return 'cht=qr'
 
1076
 
 
1077
    def data_to_url(self, data_class=None):
 
1078
        if not self.data:
 
1079
            raise NoDataGivenException()
 
1080
        return 'chl=%s' % urllib.parse.quote(self.data[0])
 
1081
 
 
1082
    def get_url_bits(self, data_class=None):
 
1083
        url_bits = Chart.get_url_bits(self, data_class=data_class)
 
1084
        if self.encoding:
 
1085
            url_bits.append('choe=%s' % self.encoding)
 
1086
        if self.ec_level:
 
1087
            url_bits.append('chld=%s%%7c%s' % (self.ec_level, self.margin))
 
1088
        return url_bits
 
1089
 
 
1090
    def set_encoding(self, encoding):
 
1091
        self.encoding = encoding
 
1092
 
 
1093
    def set_ec(self, level, margin):
 
1094
        self.ec_level = level
 
1095
        self.margin = margin
 
1096
 
 
1097
 
921
1098
class ChartGrammar(object):
922
1099
 
923
1100
    def __init__(self):
941
1118
 
942
1119
    def parse_data(self, data):
943
1120
        self.chart.data = data
944
 
        print self.chart.data
945
1121
 
946
1122
    @staticmethod
947
1123
    def get_possible_chart_types():