/+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-05-03 07:30:58 UTC
  • Revision ID: git-v1:9342edb8666dde7e843e3eb438f1f6a717aa32fc
- Really added initial unit tests
- Converted setup.py to unix file format
- warnings made when data is being clipped and when data scaling is incorrect
- max_value is now a variable
- pie and google-o-meter chart data is now on the x-axis
- More grammar work

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-2009 Gerald Kaszuba
 
6
Copyright 2007-2008 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
26
 
import urllib.request, urllib.error
 
25
import urllib2
27
26
import math
28
27
import random
29
28
import re
33
32
# Helper variables and functions
34
33
# -----------------------------------------------------------------------------
35
34
 
36
 
__version__ = '0.3.0'
 
35
__version__ = '0.2.1'
37
36
__author__ = 'Gerald Kaszuba'
38
37
 
39
38
reo_colour = re.compile('^([A-Fa-f0-9]{2,2}){3,4}$')
48
47
def _reset_warnings():
49
48
    """Helper function to reset all warnings. Used by the unit tests."""
50
49
    globals()['__warningregistry__'] = None
 
50
#def _warn(message):
 
51
#    warnings.warn_explicit(msg, warnings.UserWarning,
51
52
 
52
53
 
53
54
# Exception Classes
85
86
class UnknownChartType(PyGoogleChartException):
86
87
    pass
87
88
 
88
 
class UnknownCountryCodeException(PyGoogleChartException):
89
 
    pass
90
89
 
91
90
# Data Classes
92
91
# -----------------------------------------------------------------------------
103
102
    def float_scale_value(cls, value, range):
104
103
        lower, upper = range
105
104
        assert(upper > lower)
106
 
        scaled = (value - lower) * (cls.max_value / (upper - lower))
 
105
        scaled = (value - lower) * (float(cls.max_value) / (upper - lower))
107
106
        return scaled
108
107
 
109
108
    @classmethod
118
117
    def scale_value(cls, value, range):
119
118
        scaled = cls.int_scale_value(value, range)
120
119
        clipped = cls.clip_value(scaled)
121
 
        Data.check_clip(scaled, clipped)
122
 
        return clipped
123
 
 
124
 
    @staticmethod
125
 
    def check_clip(scaled, clipped):
126
120
        if clipped != scaled:
127
121
            warnings.warn('One or more of of your data points has been '
128
122
                'clipped because it is out of range.')
 
123
        return clipped
129
124
 
130
125
 
131
126
class SimpleData(Data):
165
160
                else:
166
161
                    raise DataOutOfRangeException()
167
162
            encoded_data.append(','.join(sub_data))
168
 
        return 'chd=t:' + '%7c'.join(encoded_data)
 
163
        return 'chd=t:' + '|'.join(encoded_data)
169
164
 
170
165
    @classmethod
171
166
    def scale_value(cls, value, range):
173
168
        # map index
174
169
        scaled = cls.float_scale_value(value, range)
175
170
        clipped = cls.clip_value(scaled)
176
 
        Data.check_clip(scaled, clipped)
177
171
        return clipped
178
172
 
179
173
 
260
254
        self.values = [str(a) for a in values]
261
255
 
262
256
    def __repr__(self):
263
 
        return '%i:%%7c%s' % (self.axis_index, '%7c'.join(self.values))
 
257
        return '%i:|%s' % (self.axis_index, '|'.join(self.values))
264
258
 
265
259
 
266
260
class RangeAxis(Axis):
284
278
    of the chart. legend requires a list that corresponds to datasets.
285
279
    """
286
280
 
287
 
    BASE_URL = 'http://chart.apis.google.com/chart'
 
281
    BASE_URL = 'http://chart.apis.google.com/chart?'
288
282
    BACKGROUND = 'bg'
289
283
    CHART = 'c'
290
284
    ALPHA = 'a'
294
288
    LINEAR_STRIPES = 'ls'
295
289
 
296
290
    def __init__(self, width, height, title=None, legend=None, colours=None,
297
 
            auto_scale=True, x_range=None, y_range=None,
298
 
            colours_within_series=None):
 
291
            auto_scale=True, x_range=None, y_range=None):
299
292
        if type(self) == Chart:
300
293
            raise AbstractClassException('This is an abstract class')
301
294
        assert(isinstance(width, int))
304
297
        self.height = height
305
298
        self.data = []
306
299
        self.set_title(title)
307
 
        self.set_title_style(None, None)
308
300
        self.set_legend(legend)
309
 
        self.set_legend_position(None)
310
301
        self.set_colours(colours)
311
 
        self.set_colours_within_series(colours_within_series)
312
302
 
313
303
        # Data for scaling.
314
304
        self.auto_scale = auto_scale  # Whether to automatically scale data
332
322
        self.markers = []
333
323
        self.line_styles = {}
334
324
        self.grid = None
335
 
        self.title_colour = None
336
 
        self.title_font_size = None
337
325
 
338
326
    # URL generation
339
327
    # -------------------------------------------------------------------------
340
 
        
 
328
 
341
329
    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):
345
330
        url_bits = self.get_url_bits(data_class=data_class)
346
 
        return '&'.join(url_bits)
 
331
        return self.BASE_URL + '&'.join(url_bits)
347
332
 
348
333
    def get_url_bits(self, data_class=None):
349
334
        url_bits = []
354
339
        # optional arguments
355
340
        if self.title:
356
341
            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))
360
342
        if 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))
 
343
            url_bits.append('chdl=%s' % '|'.join(self.legend))
364
344
        if 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))
 
345
            url_bits.append('chco=%s' % ','.join(self.colours))
368
346
        ret = self.fill_to_url()
369
347
        if ret:
370
348
            url_bits.append(ret)
371
349
        ret = self.axis_to_url()
372
350
        if ret:
373
 
            url_bits.append(ret)                    
 
351
            url_bits.append(ret)
374
352
        if self.markers:
375
 
            url_bits.append(self.markers_to_url())        
 
353
            url_bits.append(self.markers_to_url())
376
354
        if self.line_styles:
377
355
            style = []
378
 
            for index in range(max(self.line_styles) + 1):
 
356
            for index in xrange(max(self.line_styles) + 1):
379
357
                if index in self.line_styles:
380
358
                    values = self.line_styles[index]
381
359
                else:
382
360
                    values = ('1', )
383
361
                style.append(','.join(values))
384
 
            url_bits.append('chls=%s' % '%7c'.join(style))
 
362
            url_bits.append('chls=%s' % '|'.join(style))
385
363
        if self.grid:
386
364
            url_bits.append('chg=%s' % self.grid)
387
365
        return url_bits
389
367
    # Downloading
390
368
    # -------------------------------------------------------------------------
391
369
 
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())
 
370
    def download(self, file_name):
 
371
        opener = urllib2.urlopen(self.get_url())
397
372
 
398
373
        if opener.headers['content-type'] != 'image/png':
399
374
            raise BadContentTypeException('Server responded with a ' \
400
375
                'content-type of %s' % opener.headers['content-type'])
401
376
 
402
 
        open(file_name, 'wb').write(opener.read())
 
377
        open(file_name, 'wb').write(urllib.urlopen(self.get_url()).read())
403
378
 
404
379
    # Simple settings
405
380
    # -------------------------------------------------------------------------
406
381
 
407
382
    def set_title(self, title):
408
383
        if title:
409
 
            self.title = urllib.parse.quote(title)
 
384
            self.title = urllib.quote(title)
410
385
        else:
411
386
            self.title = None
412
387
 
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
 
 
421
388
    def set_legend(self, legend):
422
389
        """legend needs to be a list, tuple or None"""
423
390
        assert(isinstance(legend, list) or isinstance(legend, tuple) or
424
391
            legend is None)
425
392
        if legend:
426
 
            self.legend = [urllib.parse.quote(a) for a in legend]
 
393
            self.legend = [urllib.quote(a) for a in legend]
427
394
        else:
428
395
            self.legend = None
429
396
 
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
 
 
436
397
    # Chart colours
437
398
    # -------------------------------------------------------------------------
438
399
 
446
407
                _check_colour(col)
447
408
        self.colours = colours
448
409
 
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
 
 
459
410
    # Background/Chart colours
460
411
    # -------------------------------------------------------------------------
461
412
 
470
421
        assert(angle >= 0 and angle <= 90)
471
422
        assert(len(args) % 2 == 0)
472
423
        args = list(args)  # args is probably a tuple and we need to mutate
473
 
        for a in range(int(len(args) / 2)):
 
424
        for a in xrange(len(args) / 2):
474
425
            col = args[a * 2]
475
426
            offset = args[a * 2 + 1]
476
427
            _check_colour(col)
497
448
                areas.append('%s,%s,%s' % (area, self.fill_types[area], \
498
449
                    self.fill_area[area]))
499
450
        if areas:
500
 
            return 'chf=' + '%7c'.join(areas)
 
451
            return 'chf=' + '|'.join(areas)
501
452
 
502
453
    # Data
503
454
    # -------------------------------------------------------------------------
521
472
        else:
522
473
            return ExtendedData
523
474
 
524
 
    def _filter_none(self, data):
525
 
        return [r for r in data if r is not None]
526
 
 
527
475
    def data_x_range(self):
528
476
        """Return a 2-tuple giving the minimum and maximum x-axis
529
477
        data range.
530
478
        """
531
479
        try:
532
 
            lower = min([min(self._filter_none(s))
533
 
                         for type, s in self.annotated_data()
 
480
            lower = min([min(s) for type, s in self.annotated_data()
534
481
                         if type == 'x'])
535
 
            upper = max([max(self._filter_none(s))
536
 
                         for type, s in self.annotated_data()
 
482
            upper = max([max(s) for type, s in self.annotated_data()
537
483
                         if type == 'x'])
538
484
            return (lower, upper)
539
485
        except ValueError:
544
490
        data range.
545
491
        """
546
492
        try:
547
 
            lower = min([min(self._filter_none(s))
548
 
                         for type, s in self.annotated_data()
 
493
            lower = min([min(s) for type, s in self.annotated_data()
549
494
                         if type == 'y'])
550
 
            upper = max([max(self._filter_none(s)) + 1
551
 
                         for type, s in self.annotated_data()
 
495
            upper = max([max(s) + 1 for type, s in self.annotated_data()
552
496
                         if type == 'y'])
553
497
            return (lower, upper)
554
498
        except ValueError:
574
518
        if x_range is None:
575
519
            x_range = self.data_x_range()
576
520
            if x_range and x_range[0] > 0:
577
 
                x_range = (x_range[0], x_range[1])
 
521
                x_range = (0, x_range[1])
578
522
        self.scaled_x_range = x_range
579
523
 
580
524
        # Determine the y-axis range for scaling.
581
525
        if y_range is None:
582
526
            y_range = self.data_y_range()
583
527
            if y_range and y_range[0] > 0:
584
 
                y_range = (y_range[0], y_range[1])
 
528
                y_range = (0, y_range[1])
585
529
        self.scaled_y_range = y_range
586
530
 
587
531
        scaled_data = []
592
536
                scale_range = y_range
593
537
            elif type == 'marker-size':
594
538
                scale_range = (0, max(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)
 
539
            scaled_data.append([data_class.scale_value(v, scale_range)
 
540
                                for v in dataset])
603
541
        return scaled_data
604
542
 
605
543
    def add_data(self, data):
626
564
 
627
565
    def set_axis_labels(self, axis_type, values):
628
566
        assert(axis_type in Axis.TYPES)
629
 
        values = [urllib.parse.quote(str(a)) for a in values]
 
567
        values = [urllib.quote(a) for a in values]
630
568
        axis_index = len(self.axis)
631
569
        axis = LabelAxis(axis_index, axis_type, values)
632
570
        self.axis.append(axis)
676
614
        url_bits = []
677
615
        url_bits.append('chxt=%s' % ','.join(available_axis))
678
616
        if label_axis:
679
 
            url_bits.append('chxl=%s' % '%7c'.join(label_axis))
 
617
            url_bits.append('chxl=%s' % '|'.join(label_axis))
680
618
        if range_axis:
681
 
            url_bits.append('chxr=%s' % '%7c'.join(range_axis))
 
619
            url_bits.append('chxr=%s' % '|'.join(range_axis))
682
620
        if positions:
683
 
            url_bits.append('chxp=%s' % '%7c'.join(positions))
 
621
            url_bits.append('chxp=%s' % '|'.join(positions))
684
622
        if styles:
685
 
            url_bits.append('chxs=%s' % '%7c'.join(styles))
 
623
            url_bits.append('chxs=%s' % '|'.join(styles))
686
624
        return '&'.join(url_bits)
687
625
 
688
626
    # Markers, Ranges and Fill area (chm)
689
627
    # -------------------------------------------------------------------------
690
628
 
691
 
    def markers_to_url(self):        
692
 
        return 'chm=%s' % '%7c'.join([','.join(a) for a in self.markers])
 
629
    def markers_to_url(self):
 
630
        return 'chm=%s' % '|'.join([','.join(a) for a in self.markers])
693
631
 
694
632
    def add_marker(self, index, point, marker_type, colour, size, priority=0):
695
633
        self.markers.append((marker_type, colour, str(index), str(point), \
696
634
            str(size), str(priority)))
697
635
 
698
636
    def add_horizontal_range(self, colour, start, 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)))        
 
637
        self.markers.append(('r', colour, '1', str(start), str(stop)))
709
638
 
710
639
    def add_vertical_range(self, colour, start, stop):
711
 
        self.markers.append(('R', colour, '0', str(start), str(stop)))
 
640
        self.markers.append(('R', colour, '1', str(start), str(stop)))
712
641
 
713
642
    def add_fill_range(self, colour, index_start, index_end):
714
643
        self.markers.append(('b', colour, str(index_start), str(index_end), \
812
741
            url_bits.append('chbh=%i' % self.bar_width)
813
742
        zero_line = []
814
743
        if self.zero_lines:
815
 
            for index in range(max(self.zero_lines) + 1):
 
744
            for index in xrange(max(self.zero_lines) + 1):
816
745
                if index in self.zero_lines:
817
746
                    zero_line.append(str(self.zero_lines[index]))
818
747
                else:
906
835
                (self.__class__.__name__))
907
836
 
908
837
    def set_pie_labels(self, labels):
909
 
        self.pie_labels = [urllib.parse.quote(a) for a in labels]
 
838
        self.pie_labels = [urllib.quote(a) for a in labels]
910
839
 
911
840
    def get_url_bits(self, data_class=None):
912
841
        url_bits = Chart.get_url_bits(self, data_class=data_class)
913
842
        if self.pie_labels:
914
 
            url_bits.append('chl=%s' % '%7c'.join(self.pie_labels))
 
843
            url_bits.append('chl=%s' % '|'.join(self.pie_labels))
915
844
        return url_bits
916
845
 
917
846
    def annotated_data(self):
920
849
        for dataset in self.data:
921
850
            yield ('x', dataset)
922
851
 
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
 
 
928
852
 
929
853
class PieChart2D(PieChart):
930
854
 
966
890
        Chart.__init__(self, *args, **kwargs)
967
891
        self.geo_area = 'world'
968
892
        self.codes = []
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
 
        
 
893
 
996
894
    def type_to_url(self):
997
895
        return 'cht=t'
998
896
 
999
897
    def set_codes(self, 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)
 
898
        self.codes = codes
1035
899
 
1036
900
    def get_url_bits(self, data_class=None):
1037
901
        url_bits = Chart.get_url_bits(self, data_class=data_class)
1040
904
            url_bits.append('chld=%s' % ''.join(self.codes))
1041
905
        return url_bits
1042
906
 
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
 
 
1052
907
 
1053
908
class GoogleOMeterChart(PieChart):
1054
909
    """Inheriting from PieChart because of similar labeling"""
1063
918
        return 'cht=gom'
1064
919
 
1065
920
 
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
 
 
1098
921
class ChartGrammar(object):
1099
922
 
1100
923
    def __init__(self):
1118
941
 
1119
942
    def parse_data(self, data):
1120
943
        self.chart.data = data
 
944
        print self.chart.data
1121
945
 
1122
946
    @staticmethod
1123
947
    def get_possible_chart_types():