/+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: 2009-03-15 09:26:16 UTC
  • Revision ID: git-v1:4b470bcc49f53e8203d87d08be532215701881a6
 - #16 Allow either arguments in set_title_style()

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
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):
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
    # -------------------------------------------------------------------------
339
351
        # optional arguments
340
352
        if self.title:
341
353
            url_bits.append('chtt=%s' % self.title)
 
354
        if self.title_colour and self.title_font_size:
 
355
            url_bits.append('chts=%s,%s' % (self.title_colour, \
 
356
                self.title_font_size))
342
357
        if self.legend:
343
 
            url_bits.append('chdl=%s' % '|'.join(self.legend))
 
358
            url_bits.append('chdl=%s' % '%7c'.join(self.legend))
 
359
        if self.legend_position:
 
360
            url_bits.append('chdlp=%s' % (self.legend_position))
344
361
        if self.colours:
345
 
            url_bits.append('chco=%s' % ','.join(self.colours))
 
362
            url_bits.append('chco=%s' % ','.join(self.colours))            
 
363
        if self.colours_within_series:
 
364
            url_bits.append('chco=%s' % '%7c'.join(self.colours_within_series))
346
365
        ret = self.fill_to_url()
347
366
        if ret:
348
367
            url_bits.append(ret)
349
368
        ret = self.axis_to_url()
350
369
        if ret:
351
 
            url_bits.append(ret)
 
370
            url_bits.append(ret)                    
352
371
        if self.markers:
353
 
            url_bits.append(self.markers_to_url())
 
372
            url_bits.append(self.markers_to_url())        
354
373
        if self.line_styles:
355
374
            style = []
356
375
            for index in xrange(max(self.line_styles) + 1):
359
378
                else:
360
379
                    values = ('1', )
361
380
                style.append(','.join(values))
362
 
            url_bits.append('chls=%s' % '|'.join(style))
 
381
            url_bits.append('chls=%s' % '%7c'.join(style))
363
382
        if self.grid:
364
383
            url_bits.append('chg=%s' % self.grid)
365
384
        return url_bits
374
393
            raise BadContentTypeException('Server responded with a ' \
375
394
                'content-type of %s' % opener.headers['content-type'])
376
395
 
377
 
        open(file_name, 'wb').write(urllib.urlopen(self.get_url()).read())
 
396
        open(file_name, 'wb').write(opener.read())
378
397
 
379
398
    # Simple settings
380
399
    # -------------------------------------------------------------------------
385
404
        else:
386
405
            self.title = None
387
406
 
 
407
    def set_title_style(self, colour=None, font_size=None):
 
408
        if not colour is None:
 
409
            _check_colour(colour)
 
410
        if not colour and not font_size:
 
411
            return
 
412
        self.title_colour = colour or '333333'
 
413
        self.title_font_size = font_size or 13.5
 
414
 
388
415
    def set_legend(self, legend):
389
416
        """legend needs to be a list, tuple or None"""
390
417
        assert(isinstance(legend, list) or isinstance(legend, tuple) or
394
421
        else:
395
422
            self.legend = None
396
423
 
 
424
    def set_legend_position(self, legend_position):
 
425
        if legend_position:
 
426
            self.legend_position = urllib.quote(legend_position)
 
427
        else:    
 
428
            self.legend_position = None
 
429
 
397
430
    # Chart colours
398
431
    # -------------------------------------------------------------------------
399
432
 
407
440
                _check_colour(col)
408
441
        self.colours = colours
409
442
 
 
443
    def set_colours_within_series(self, colours):
 
444
        # colours needs to be a list, tuple or None
 
445
        assert(isinstance(colours, list) or isinstance(colours, tuple) or
 
446
            colours is None)
 
447
        # make sure the colours are in the right format
 
448
        if colours:
 
449
            for col in colours:
 
450
                _check_colour(col)
 
451
        self.colours_within_series = colours        
 
452
 
410
453
    # Background/Chart colours
411
454
    # -------------------------------------------------------------------------
412
455
 
421
464
        assert(angle >= 0 and angle <= 90)
422
465
        assert(len(args) % 2 == 0)
423
466
        args = list(args)  # args is probably a tuple and we need to mutate
424
 
        for a in xrange(len(args) / 2):
 
467
        for a in xrange(int(len(args) / 2)):
425
468
            col = args[a * 2]
426
469
            offset = args[a * 2 + 1]
427
470
            _check_colour(col)
448
491
                areas.append('%s,%s,%s' % (area, self.fill_types[area], \
449
492
                    self.fill_area[area]))
450
493
        if areas:
451
 
            return 'chf=' + '|'.join(areas)
 
494
            return 'chf=' + '%7c'.join(areas)
452
495
 
453
496
    # Data
454
497
    # -------------------------------------------------------------------------
472
515
        else:
473
516
            return ExtendedData
474
517
 
 
518
    def _filter_none(self, data):
 
519
        return [r for r in data if r is not None]
 
520
 
475
521
    def data_x_range(self):
476
522
        """Return a 2-tuple giving the minimum and maximum x-axis
477
523
        data range.
478
524
        """
479
525
        try:
480
 
            lower = min([min(s) for type, s in self.annotated_data()
 
526
            lower = min([min(self._filter_none(s))
 
527
                         for type, s in self.annotated_data()
481
528
                         if type == 'x'])
482
 
            upper = max([max(s) for type, s in self.annotated_data()
 
529
            upper = max([max(self._filter_none(s))
 
530
                         for type, s in self.annotated_data()
483
531
                         if type == 'x'])
484
532
            return (lower, upper)
485
533
        except ValueError:
490
538
        data range.
491
539
        """
492
540
        try:
493
 
            lower = min([min(s) for type, s in self.annotated_data()
 
541
            lower = min([min(self._filter_none(s))
 
542
                         for type, s in self.annotated_data()
494
543
                         if type == 'y'])
495
 
            upper = max([max(s) + 1 for type, s in self.annotated_data()
 
544
            upper = max([max(self._filter_none(s)) + 1
 
545
                         for type, s in self.annotated_data()
496
546
                         if type == 'y'])
497
547
            return (lower, upper)
498
548
        except ValueError:
518
568
        if x_range is None:
519
569
            x_range = self.data_x_range()
520
570
            if x_range and x_range[0] > 0:
521
 
                x_range = (0, x_range[1])
 
571
                x_range = (x_range[0], x_range[1])
522
572
        self.scaled_x_range = x_range
523
573
 
524
574
        # Determine the y-axis range for scaling.
525
575
        if y_range is None:
526
576
            y_range = self.data_y_range()
527
577
            if y_range and y_range[0] > 0:
528
 
                y_range = (0, y_range[1])
 
578
                y_range = (y_range[0], y_range[1])
529
579
        self.scaled_y_range = y_range
530
580
 
531
581
        scaled_data = []
536
586
                scale_range = y_range
537
587
            elif type == 'marker-size':
538
588
                scale_range = (0, max(dataset))
539
 
            scaled_data.append([data_class.scale_value(v, scale_range)
540
 
                                for v in dataset])
 
589
            scaled_dataset = []
 
590
            for v in dataset:
 
591
                if v is None:
 
592
                    scaled_dataset.append(None)
 
593
                else:
 
594
                    scaled_dataset.append(
 
595
                        data_class.scale_value(v, scale_range))
 
596
            scaled_data.append(scaled_dataset)
541
597
        return scaled_data
542
598
 
543
599
    def add_data(self, data):
564
620
 
565
621
    def set_axis_labels(self, axis_type, values):
566
622
        assert(axis_type in Axis.TYPES)
567
 
        values = [urllib.quote(a) for a in values]
 
623
        values = [urllib.quote(str(a)) for a in values]
568
624
        axis_index = len(self.axis)
569
625
        axis = LabelAxis(axis_index, axis_type, values)
570
626
        self.axis.append(axis)
614
670
        url_bits = []
615
671
        url_bits.append('chxt=%s' % ','.join(available_axis))
616
672
        if label_axis:
617
 
            url_bits.append('chxl=%s' % '|'.join(label_axis))
 
673
            url_bits.append('chxl=%s' % '%7c'.join(label_axis))
618
674
        if range_axis:
619
 
            url_bits.append('chxr=%s' % '|'.join(range_axis))
 
675
            url_bits.append('chxr=%s' % '%7c'.join(range_axis))
620
676
        if positions:
621
 
            url_bits.append('chxp=%s' % '|'.join(positions))
 
677
            url_bits.append('chxp=%s' % '%7c'.join(positions))
622
678
        if styles:
623
 
            url_bits.append('chxs=%s' % '|'.join(styles))
 
679
            url_bits.append('chxs=%s' % '%7c'.join(styles))
624
680
        return '&'.join(url_bits)
625
681
 
626
682
    # Markers, Ranges and Fill area (chm)
627
683
    # -------------------------------------------------------------------------
628
684
 
629
 
    def markers_to_url(self):
630
 
        return 'chm=%s' % '|'.join([','.join(a) for a in self.markers])
 
685
    def markers_to_url(self):        
 
686
        return 'chm=%s' % '%7c'.join([','.join(a) for a in self.markers])
631
687
 
632
688
    def add_marker(self, index, point, marker_type, colour, size, priority=0):
633
689
        self.markers.append((marker_type, colour, str(index), str(point), \
634
690
            str(size), str(priority)))
635
691
 
636
692
    def add_horizontal_range(self, colour, start, stop):
637
 
        self.markers.append(('r', colour, '1', str(start), str(stop)))
 
693
        self.markers.append(('r', colour, '0', str(start), str(stop)))
 
694
 
 
695
    def add_data_line(self, colour, data_set, size, priority=0):
 
696
        self.markers.append(('D', colour, str(data_set), '0', str(size), \
 
697
            str(priority)))
 
698
 
 
699
    def add_marker_text(self, string, colour, data_set, data_point, size, \
 
700
            priority=0):
 
701
        self.markers.append((str(string), colour, str(data_set), \
 
702
            str(data_point), str(size), str(priority)))        
638
703
 
639
704
    def add_vertical_range(self, colour, start, stop):
640
 
        self.markers.append(('R', colour, '1', str(start), str(stop)))
 
705
        self.markers.append(('R', colour, '0', str(start), str(stop)))
641
706
 
642
707
    def add_fill_range(self, colour, index_start, index_end):
643
708
        self.markers.append(('b', colour, str(index_start), str(index_end), \
840
905
    def get_url_bits(self, data_class=None):
841
906
        url_bits = Chart.get_url_bits(self, data_class=data_class)
842
907
        if self.pie_labels:
843
 
            url_bits.append('chl=%s' % '|'.join(self.pie_labels))
 
908
            url_bits.append('chl=%s' % '%7c'.join(self.pie_labels))
844
909
        return url_bits
845
910
 
846
911
    def annotated_data(self):
849
914
        for dataset in self.data:
850
915
            yield ('x', dataset)
851
916
 
 
917
    def scaled_data(self, data_class, x_range=None, y_range=None):
 
918
        if not x_range:
 
919
            x_range = [0, sum(self.data[0])]
 
920
        return Chart.scaled_data(self, data_class, x_range, self.y_range)
 
921
 
852
922
 
853
923
class PieChart2D(PieChart):
854
924
 
890
960
        Chart.__init__(self, *args, **kwargs)
891
961
        self.geo_area = 'world'
892
962
        self.codes = []
893
 
 
 
963
        self.__areas = ('africa', 'asia', 'europe', 'middle_east',
 
964
            'south_america', 'usa', 'world')
 
965
        self.__ccodes = (
 
966
            'AD', 'AE', 'AF', 'AG', 'AI', 'AL', 'AM', 'AN', 'AO', 'AQ', 'AR',
 
967
            'AS', 'AT', 'AU', 'AW', 'AX', 'AZ', 'BA', 'BB', 'BD', 'BE', 'BF',
 
968
            'BG', 'BH', 'BI', 'BJ', 'BL', 'BM', 'BN', 'BO', 'BR', 'BS', 'BT',
 
969
            'BV', 'BW', 'BY', 'BZ', 'CA', 'CC', 'CD', 'CF', 'CG', 'CH', 'CI',
 
970
            'CK', 'CL', 'CM', 'CN', 'CO', 'CR', 'CU', 'CV', 'CX', 'CY', 'CZ',
 
971
            'DE', 'DJ', 'DK', 'DM', 'DO', 'DZ', 'EC', 'EE', 'EG', 'EH', 'ER',
 
972
            'ES', 'ET', 'FI', 'FJ', 'FK', 'FM', 'FO', 'FR', 'GA', 'GB', 'GD',
 
973
            'GE', 'GF', 'GG', 'GH', 'GI', 'GL', 'GM', 'GN', 'GP', 'GQ', 'GR',
 
974
            'GS', 'GT', 'GU', 'GW', 'GY', 'HK', 'HM', 'HN', 'HR', 'HT', 'HU',
 
975
            'ID', 'IE', 'IL', 'IM', 'IN', 'IO', 'IQ', 'IR', 'IS', 'IT', 'JE',
 
976
            'JM', 'JO', 'JP', 'KE', 'KG', 'KH', 'KI', 'KM', 'KN', 'KP', 'KR',
 
977
            'KW', 'KY', 'KZ', 'LA', 'LB', 'LC', 'LI', 'LK', 'LR', 'LS', 'LT',
 
978
            'LU', 'LV', 'LY', 'MA', 'MC', 'MD', 'ME', 'MF', 'MG', 'MH', 'MK',
 
979
            'ML', 'MM', 'MN', 'MO', 'MP', 'MQ', 'MR', 'MS', 'MT', 'MU', 'MV',
 
980
            'MW', 'MX', 'MY', 'MZ', 'NA', 'NC', 'NE', 'NF', 'NG', 'NI', 'NL',
 
981
            'NO', 'NP', 'NR', 'NU', 'NZ', 'OM', 'PA', 'PE', 'PF', 'PG', 'PH',
 
982
            'PK', 'PL', 'PM', 'PN', 'PR', 'PS', 'PT', 'PW', 'PY', 'QA', 'RE',
 
983
            'RO', 'RS', 'RU', 'RW', 'SA', 'SB', 'SC', 'SD', 'SE', 'SG', 'SH',
 
984
            'SI', 'SJ', 'SK', 'SL', 'SM', 'SN', 'SO', 'SR', 'ST', 'SV', 'SY',
 
985
            'SZ', 'TC', 'TD', 'TF', 'TG', 'TH', 'TJ', 'TK', 'TL', 'TM', 'TN',
 
986
            'TO', 'TR', 'TT', 'TV', 'TW', 'TZ', 'UA', 'UG', 'UM', 'US', 'UY',
 
987
            'UZ', 'VA', 'VC', 'VE', 'VG', 'VI', 'VN', 'VU', 'WF', 'WS', 'YE',
 
988
            'YT', 'ZA', 'ZM', 'ZW')
 
989
        
894
990
    def type_to_url(self):
895
991
        return 'cht=t'
896
992
 
897
993
    def set_codes(self, codes):
898
 
        self.codes = codes
 
994
        '''Set the country code map for the data.
 
995
        Codes given in a list.
 
996
 
 
997
        i.e. DE - Germany
 
998
             AT - Austria
 
999
             US - United States
 
1000
        '''
 
1001
 
 
1002
        codemap = ''
 
1003
        
 
1004
        for cc in codes:
 
1005
            cc = cc.upper()
 
1006
            if cc in self.__ccodes:
 
1007
                codemap += cc
 
1008
            else:
 
1009
                raise UnknownCountryCodeException(cc)
 
1010
            
 
1011
        self.codes = codemap
 
1012
 
 
1013
    def set_geo_area(self, area):
 
1014
        '''Sets the geo area for the map.
 
1015
 
 
1016
        * africa
 
1017
        * asia
 
1018
        * europe
 
1019
        * middle_east
 
1020
        * south_america
 
1021
        * usa
 
1022
        * world
 
1023
        '''
 
1024
        
 
1025
        if area in self.__areas:
 
1026
            self.geo_area = area
 
1027
        else:
 
1028
            raise UnknownChartType('Unknown chart type for maps: %s' %area)
899
1029
 
900
1030
    def get_url_bits(self, data_class=None):
901
1031
        url_bits = Chart.get_url_bits(self, data_class=data_class)
904
1034
            url_bits.append('chld=%s' % ''.join(self.codes))
905
1035
        return url_bits
906
1036
 
 
1037
    def add_data_dict(self, datadict):
 
1038
        '''Sets the data and country codes via a dictionary.
 
1039
 
 
1040
        i.e. {'DE': 50, 'GB': 30, 'AT': 70}
 
1041
        '''
 
1042
 
 
1043
        self.set_codes(datadict.keys())
 
1044
        self.add_data(datadict.values())
 
1045
 
907
1046
 
908
1047
class GoogleOMeterChart(PieChart):
909
1048
    """Inheriting from PieChart because of similar labeling"""
918
1057
        return 'cht=gom'
919
1058
 
920
1059
 
 
1060
class QRChart(Chart):
 
1061
 
 
1062
    def __init__(self, *args, **kwargs):
 
1063
        Chart.__init__(self, *args, **kwargs)
 
1064
        self.encoding = None
 
1065
        self.ec_level = None
 
1066
        self.margin = None
 
1067
 
 
1068
    def type_to_url(self):
 
1069
        return 'cht=qr'
 
1070
 
 
1071
    def data_to_url(self, data_class=None):
 
1072
        if not self.data:
 
1073
            raise NoDataGivenException()
 
1074
        return 'chl=%s' % urllib.quote(self.data[0])
 
1075
 
 
1076
    def get_url_bits(self, data_class=None):
 
1077
        url_bits = Chart.get_url_bits(self, data_class=data_class)
 
1078
        if self.encoding:
 
1079
            url_bits.append('choe=%s' % self.encoding)
 
1080
        if self.ec_level:
 
1081
            url_bits.append('chld=%s%%7c%s' % (self.ec_level, self.margin))
 
1082
        return url_bits
 
1083
 
 
1084
    def set_encoding(self, encoding):
 
1085
        self.encoding = encoding
 
1086
 
 
1087
    def set_ec(self, level, margin):
 
1088
        self.ec_level = level
 
1089
        self.margin = margin
 
1090
 
 
1091
 
921
1092
class ChartGrammar(object):
922
1093
 
923
1094
    def __init__(self):
941
1112
 
942
1113
    def parse_data(self, data):
943
1114
        self.chart.data = data
944
 
        print self.chart.data
945
1115
 
946
1116
    @staticmethod
947
1117
    def get_possible_chart_types():