/+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
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):
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
    # -------------------------------------------------------------------------
351
339
        # optional arguments
352
340
        if self.title:
353
341
            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))
357
342
        if 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))
 
343
            url_bits.append('chdl=%s' % '|'.join(self.legend))
361
344
        if 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))
 
345
            url_bits.append('chco=%s' % ','.join(self.colours))
365
346
        ret = self.fill_to_url()
366
347
        if ret:
367
348
            url_bits.append(ret)
368
349
        ret = self.axis_to_url()
369
350
        if ret:
370
 
            url_bits.append(ret)                    
 
351
            url_bits.append(ret)
371
352
        if self.markers:
372
 
            url_bits.append(self.markers_to_url())        
 
353
            url_bits.append(self.markers_to_url())
373
354
        if self.line_styles:
374
355
            style = []
375
356
            for index in xrange(max(self.line_styles) + 1):
378
359
                else:
379
360
                    values = ('1', )
380
361
                style.append(','.join(values))
381
 
            url_bits.append('chls=%s' % '%7c'.join(style))
 
362
            url_bits.append('chls=%s' % '|'.join(style))
382
363
        if self.grid:
383
364
            url_bits.append('chg=%s' % self.grid)
384
365
        return url_bits
393
374
            raise BadContentTypeException('Server responded with a ' \
394
375
                'content-type of %s' % opener.headers['content-type'])
395
376
 
396
 
        open(file_name, 'wb').write(opener.read())
 
377
        open(file_name, 'wb').write(urllib.urlopen(self.get_url()).read())
397
378
 
398
379
    # Simple settings
399
380
    # -------------------------------------------------------------------------
404
385
        else:
405
386
            self.title = None
406
387
 
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
 
 
415
388
    def set_legend(self, legend):
416
389
        """legend needs to be a list, tuple or None"""
417
390
        assert(isinstance(legend, list) or isinstance(legend, tuple) or
421
394
        else:
422
395
            self.legend = None
423
396
 
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
 
 
430
397
    # Chart colours
431
398
    # -------------------------------------------------------------------------
432
399
 
440
407
                _check_colour(col)
441
408
        self.colours = colours
442
409
 
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
 
 
453
410
    # Background/Chart colours
454
411
    # -------------------------------------------------------------------------
455
412
 
464
421
        assert(angle >= 0 and angle <= 90)
465
422
        assert(len(args) % 2 == 0)
466
423
        args = list(args)  # args is probably a tuple and we need to mutate
467
 
        for a in xrange(int(len(args) / 2)):
 
424
        for a in xrange(len(args) / 2):
468
425
            col = args[a * 2]
469
426
            offset = args[a * 2 + 1]
470
427
            _check_colour(col)
491
448
                areas.append('%s,%s,%s' % (area, self.fill_types[area], \
492
449
                    self.fill_area[area]))
493
450
        if areas:
494
 
            return 'chf=' + '%7c'.join(areas)
 
451
            return 'chf=' + '|'.join(areas)
495
452
 
496
453
    # Data
497
454
    # -------------------------------------------------------------------------
515
472
        else:
516
473
            return ExtendedData
517
474
 
518
 
    def _filter_none(self, data):
519
 
        return [r for r in data if r is not None]
520
 
 
521
475
    def data_x_range(self):
522
476
        """Return a 2-tuple giving the minimum and maximum x-axis
523
477
        data range.
524
478
        """
525
479
        try:
526
 
            lower = min([min(self._filter_none(s))
527
 
                         for type, s in self.annotated_data()
 
480
            lower = min([min(s) for type, s in self.annotated_data()
528
481
                         if type == 'x'])
529
 
            upper = max([max(self._filter_none(s))
530
 
                         for type, s in self.annotated_data()
 
482
            upper = max([max(s) for type, s in self.annotated_data()
531
483
                         if type == 'x'])
532
484
            return (lower, upper)
533
485
        except ValueError:
538
490
        data range.
539
491
        """
540
492
        try:
541
 
            lower = min([min(self._filter_none(s))
542
 
                         for type, s in self.annotated_data()
 
493
            lower = min([min(s) for type, s in self.annotated_data()
543
494
                         if type == 'y'])
544
 
            upper = max([max(self._filter_none(s)) + 1
545
 
                         for type, s in self.annotated_data()
 
495
            upper = max([max(s) + 1 for type, s in self.annotated_data()
546
496
                         if type == 'y'])
547
497
            return (lower, upper)
548
498
        except ValueError:
568
518
        if x_range is None:
569
519
            x_range = self.data_x_range()
570
520
            if x_range and x_range[0] > 0:
571
 
                x_range = (x_range[0], x_range[1])
 
521
                x_range = (0, x_range[1])
572
522
        self.scaled_x_range = x_range
573
523
 
574
524
        # Determine the y-axis range for scaling.
575
525
        if y_range is None:
576
526
            y_range = self.data_y_range()
577
527
            if y_range and y_range[0] > 0:
578
 
                y_range = (y_range[0], y_range[1])
 
528
                y_range = (0, y_range[1])
579
529
        self.scaled_y_range = y_range
580
530
 
581
531
        scaled_data = []
586
536
                scale_range = y_range
587
537
            elif type == 'marker-size':
588
538
                scale_range = (0, max(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)
 
539
            scaled_data.append([data_class.scale_value(v, scale_range)
 
540
                                for v in dataset])
597
541
        return scaled_data
598
542
 
599
543
    def add_data(self, data):
620
564
 
621
565
    def set_axis_labels(self, axis_type, values):
622
566
        assert(axis_type in Axis.TYPES)
623
 
        values = [urllib.quote(str(a)) for a in values]
 
567
        values = [urllib.quote(a) for a in values]
624
568
        axis_index = len(self.axis)
625
569
        axis = LabelAxis(axis_index, axis_type, values)
626
570
        self.axis.append(axis)
670
614
        url_bits = []
671
615
        url_bits.append('chxt=%s' % ','.join(available_axis))
672
616
        if label_axis:
673
 
            url_bits.append('chxl=%s' % '%7c'.join(label_axis))
 
617
            url_bits.append('chxl=%s' % '|'.join(label_axis))
674
618
        if range_axis:
675
 
            url_bits.append('chxr=%s' % '%7c'.join(range_axis))
 
619
            url_bits.append('chxr=%s' % '|'.join(range_axis))
676
620
        if positions:
677
 
            url_bits.append('chxp=%s' % '%7c'.join(positions))
 
621
            url_bits.append('chxp=%s' % '|'.join(positions))
678
622
        if styles:
679
 
            url_bits.append('chxs=%s' % '%7c'.join(styles))
 
623
            url_bits.append('chxs=%s' % '|'.join(styles))
680
624
        return '&'.join(url_bits)
681
625
 
682
626
    # Markers, Ranges and Fill area (chm)
683
627
    # -------------------------------------------------------------------------
684
628
 
685
 
    def markers_to_url(self):        
686
 
        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])
687
631
 
688
632
    def add_marker(self, index, point, marker_type, colour, size, priority=0):
689
633
        self.markers.append((marker_type, colour, str(index), str(point), \
690
634
            str(size), str(priority)))
691
635
 
692
636
    def add_horizontal_range(self, colour, start, 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)))        
 
637
        self.markers.append(('r', colour, '1', str(start), str(stop)))
703
638
 
704
639
    def add_vertical_range(self, colour, start, stop):
705
 
        self.markers.append(('R', colour, '0', str(start), str(stop)))
 
640
        self.markers.append(('R', colour, '1', str(start), str(stop)))
706
641
 
707
642
    def add_fill_range(self, colour, index_start, index_end):
708
643
        self.markers.append(('b', colour, str(index_start), str(index_end), \
905
840
    def get_url_bits(self, data_class=None):
906
841
        url_bits = Chart.get_url_bits(self, data_class=data_class)
907
842
        if self.pie_labels:
908
 
            url_bits.append('chl=%s' % '%7c'.join(self.pie_labels))
 
843
            url_bits.append('chl=%s' % '|'.join(self.pie_labels))
909
844
        return url_bits
910
845
 
911
846
    def annotated_data(self):
914
849
        for dataset in self.data:
915
850
            yield ('x', dataset)
916
851
 
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
 
 
922
852
 
923
853
class PieChart2D(PieChart):
924
854
 
960
890
        Chart.__init__(self, *args, **kwargs)
961
891
        self.geo_area = 'world'
962
892
        self.codes = []
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
 
        
 
893
 
990
894
    def type_to_url(self):
991
895
        return 'cht=t'
992
896
 
993
897
    def set_codes(self, 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)
 
898
        self.codes = codes
1029
899
 
1030
900
    def get_url_bits(self, data_class=None):
1031
901
        url_bits = Chart.get_url_bits(self, data_class=data_class)
1034
904
            url_bits.append('chld=%s' % ''.join(self.codes))
1035
905
        return url_bits
1036
906
 
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
 
 
1046
907
 
1047
908
class GoogleOMeterChart(PieChart):
1048
909
    """Inheriting from PieChart because of similar labeling"""
1057
918
        return 'cht=gom'
1058
919
 
1059
920
 
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
 
 
1092
921
class ChartGrammar(object):
1093
922
 
1094
923
    def __init__(self):
1112
941
 
1113
942
    def parse_data(self, data):
1114
943
        self.chart.data = data
 
944
        print self.chart.data
1115
945
 
1116
946
    @staticmethod
1117
947
    def get_possible_chart_types():