/+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-08-23 06:46:04 UTC
  • Revision ID: git-v1:df4fb7845ec5ed0e89d6b485a0c6babd75ac5ff3
 - Bug fixed when automatic scaling is on and None values are in a data set (#5) (Alec Thomas)

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}$')
85
84
class UnknownChartType(PyGoogleChartException):
86
85
    pass
87
86
 
88
 
class UnknownCountryCodeException(PyGoogleChartException):
89
 
    pass
90
87
 
91
88
# Data Classes
92
89
# -----------------------------------------------------------------------------
103
100
    def float_scale_value(cls, value, range):
104
101
        lower, upper = range
105
102
        assert(upper > lower)
106
 
        scaled = (value - lower) * (cls.max_value / (upper - lower))
 
103
        scaled = (value - lower) * (float(cls.max_value) / (upper - lower))
107
104
        return scaled
108
105
 
109
106
    @classmethod
165
162
                else:
166
163
                    raise DataOutOfRangeException()
167
164
            encoded_data.append(','.join(sub_data))
168
 
        return 'chd=t:' + '%7c'.join(encoded_data)
 
165
        return 'chd=t:' + '|'.join(encoded_data)
169
166
 
170
167
    @classmethod
171
168
    def scale_value(cls, value, range):
260
257
        self.values = [str(a) for a in values]
261
258
 
262
259
    def __repr__(self):
263
 
        return '%i:%%7c%s' % (self.axis_index, '%7c'.join(self.values))
 
260
        return '%i:|%s' % (self.axis_index, '|'.join(self.values))
264
261
 
265
262
 
266
263
class RangeAxis(Axis):
284
281
    of the chart. legend requires a list that corresponds to datasets.
285
282
    """
286
283
 
287
 
    BASE_URL = 'http://chart.apis.google.com/chart'
 
284
    BASE_URL = 'http://chart.apis.google.com/chart?'
288
285
    BACKGROUND = 'bg'
289
286
    CHART = 'c'
290
287
    ALPHA = 'a'
304
301
        self.height = height
305
302
        self.data = []
306
303
        self.set_title(title)
307
 
        self.set_title_style(None, None)
308
304
        self.set_legend(legend)
309
305
        self.set_legend_position(None)
310
306
        self.set_colours(colours)
332
328
        self.markers = []
333
329
        self.line_styles = {}
334
330
        self.grid = None
335
 
        self.title_colour = None
336
 
        self.title_font_size = None
337
331
 
338
332
    # URL generation
339
333
    # -------------------------------------------------------------------------
340
 
        
 
334
 
341
335
    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
336
        url_bits = self.get_url_bits(data_class=data_class)
346
 
        return '&'.join(url_bits)
 
337
        return self.BASE_URL + '&'.join(url_bits)
347
338
 
348
339
    def get_url_bits(self, data_class=None):
349
340
        url_bits = []
354
345
        # optional arguments
355
346
        if self.title:
356
347
            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
348
        if self.legend:
361
 
            url_bits.append('chdl=%s' % '%7c'.join(self.legend))
 
349
            url_bits.append('chdl=%s' % '|'.join(self.legend))
362
350
        if self.legend_position:
363
351
            url_bits.append('chdlp=%s' % (self.legend_position))
364
352
        if self.colours:
365
353
            url_bits.append('chco=%s' % ','.join(self.colours))            
366
354
        if self.colours_within_series:
367
 
            url_bits.append('chco=%s' % '%7c'.join(self.colours_within_series))
 
355
            url_bits.append('chco=%s' % '|'.join(self.colours_within_series))
368
356
        ret = self.fill_to_url()
369
357
        if ret:
370
358
            url_bits.append(ret)
375
363
            url_bits.append(self.markers_to_url())        
376
364
        if self.line_styles:
377
365
            style = []
378
 
            for index in range(max(self.line_styles) + 1):
 
366
            for index in xrange(max(self.line_styles) + 1):
379
367
                if index in self.line_styles:
380
368
                    values = self.line_styles[index]
381
369
                else:
382
370
                    values = ('1', )
383
371
                style.append(','.join(values))
384
 
            url_bits.append('chls=%s' % '%7c'.join(style))
 
372
            url_bits.append('chls=%s' % '|'.join(style))
385
373
        if self.grid:
386
374
            url_bits.append('chg=%s' % self.grid)
387
375
        return url_bits
389
377
    # Downloading
390
378
    # -------------------------------------------------------------------------
391
379
 
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())
 
380
    def download(self, file_name):
 
381
        opener = urllib2.urlopen(self.get_url())
397
382
 
398
383
        if opener.headers['content-type'] != 'image/png':
399
384
            raise BadContentTypeException('Server responded with a ' \
400
385
                'content-type of %s' % opener.headers['content-type'])
401
386
 
402
 
        open(file_name, 'wb').write(opener.read())
 
387
        open(file_name, 'wb').write(urllib.urlopen(self.get_url()).read())
403
388
 
404
389
    # Simple settings
405
390
    # -------------------------------------------------------------------------
406
391
 
407
392
    def set_title(self, title):
408
393
        if title:
409
 
            self.title = urllib.parse.quote(title)
 
394
            self.title = urllib.quote(title)
410
395
        else:
411
396
            self.title = None
412
397
 
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
398
    def set_legend(self, legend):
422
399
        """legend needs to be a list, tuple or None"""
423
400
        assert(isinstance(legend, list) or isinstance(legend, tuple) or
424
401
            legend is None)
425
402
        if legend:
426
 
            self.legend = [urllib.parse.quote(a) for a in legend]
 
403
            self.legend = [urllib.quote(a) for a in legend]
427
404
        else:
428
405
            self.legend = None
429
406
 
430
407
    def set_legend_position(self, legend_position):
431
408
        if legend_position:
432
 
            self.legend_position = urllib.parse.quote(legend_position)
 
409
            self.legend_position = urllib.quote(legend_position)
433
410
        else:    
434
411
            self.legend_position = None
435
412
 
470
447
        assert(angle >= 0 and angle <= 90)
471
448
        assert(len(args) % 2 == 0)
472
449
        args = list(args)  # args is probably a tuple and we need to mutate
473
 
        for a in range(int(len(args) / 2)):
 
450
        for a in xrange(len(args) / 2):
474
451
            col = args[a * 2]
475
452
            offset = args[a * 2 + 1]
476
453
            _check_colour(col)
497
474
                areas.append('%s,%s,%s' % (area, self.fill_types[area], \
498
475
                    self.fill_area[area]))
499
476
        if areas:
500
 
            return 'chf=' + '%7c'.join(areas)
 
477
            return 'chf=' + '|'.join(areas)
501
478
 
502
479
    # Data
503
480
    # -------------------------------------------------------------------------
574
551
        if x_range is None:
575
552
            x_range = self.data_x_range()
576
553
            if x_range and x_range[0] > 0:
577
 
                x_range = (x_range[0], x_range[1])
 
554
                x_range = (0, x_range[1])
578
555
        self.scaled_x_range = x_range
579
556
 
580
557
        # Determine the y-axis range for scaling.
581
558
        if y_range is None:
582
559
            y_range = self.data_y_range()
583
560
            if y_range and y_range[0] > 0:
584
 
                y_range = (y_range[0], y_range[1])
 
561
                y_range = (0, y_range[1])
585
562
        self.scaled_y_range = y_range
586
563
 
587
564
        scaled_data = []
626
603
 
627
604
    def set_axis_labels(self, axis_type, values):
628
605
        assert(axis_type in Axis.TYPES)
629
 
        values = [urllib.parse.quote(str(a)) for a in values]
 
606
        values = [urllib.quote(a) for a in values]
630
607
        axis_index = len(self.axis)
631
608
        axis = LabelAxis(axis_index, axis_type, values)
632
609
        self.axis.append(axis)
676
653
        url_bits = []
677
654
        url_bits.append('chxt=%s' % ','.join(available_axis))
678
655
        if label_axis:
679
 
            url_bits.append('chxl=%s' % '%7c'.join(label_axis))
 
656
            url_bits.append('chxl=%s' % '|'.join(label_axis))
680
657
        if range_axis:
681
 
            url_bits.append('chxr=%s' % '%7c'.join(range_axis))
 
658
            url_bits.append('chxr=%s' % '|'.join(range_axis))
682
659
        if positions:
683
 
            url_bits.append('chxp=%s' % '%7c'.join(positions))
 
660
            url_bits.append('chxp=%s' % '|'.join(positions))
684
661
        if styles:
685
 
            url_bits.append('chxs=%s' % '%7c'.join(styles))
 
662
            url_bits.append('chxs=%s' % '|'.join(styles))
686
663
        return '&'.join(url_bits)
687
664
 
688
665
    # Markers, Ranges and Fill area (chm)
689
666
    # -------------------------------------------------------------------------
690
667
 
691
668
    def markers_to_url(self):        
692
 
        return 'chm=%s' % '%7c'.join([','.join(a) for a in self.markers])
 
669
        return 'chm=%s' % '|'.join([','.join(a) for a in self.markers])
693
670
 
694
671
    def add_marker(self, index, point, marker_type, colour, size, priority=0):
695
672
        self.markers.append((marker_type, colour, str(index), str(point), \
699
676
        self.markers.append(('r', colour, '0', str(start), str(stop)))
700
677
 
701
678
    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)))
 
679
        self.markers.append(('D', colour, str(data_set), '0', str(size), str(priority)))
704
680
 
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)))        
 
681
    def add_marker_text(self, string, colour, data_set, data_point, size, priority=0):
 
682
        self.markers.append((str(string), colour, str(data_set), str(data_point), str(size), str(priority)))        
709
683
 
710
684
    def add_vertical_range(self, colour, start, stop):
711
685
        self.markers.append(('R', colour, '0', str(start), str(stop)))
812
786
            url_bits.append('chbh=%i' % self.bar_width)
813
787
        zero_line = []
814
788
        if self.zero_lines:
815
 
            for index in range(max(self.zero_lines) + 1):
 
789
            for index in xrange(max(self.zero_lines) + 1):
816
790
                if index in self.zero_lines:
817
791
                    zero_line.append(str(self.zero_lines[index]))
818
792
                else:
906
880
                (self.__class__.__name__))
907
881
 
908
882
    def set_pie_labels(self, labels):
909
 
        self.pie_labels = [urllib.parse.quote(a) for a in labels]
 
883
        self.pie_labels = [urllib.quote(a) for a in labels]
910
884
 
911
885
    def get_url_bits(self, data_class=None):
912
886
        url_bits = Chart.get_url_bits(self, data_class=data_class)
913
887
        if self.pie_labels:
914
 
            url_bits.append('chl=%s' % '%7c'.join(self.pie_labels))
 
888
            url_bits.append('chl=%s' % '|'.join(self.pie_labels))
915
889
        return url_bits
916
890
 
917
891
    def annotated_data(self):
920
894
        for dataset in self.data:
921
895
            yield ('x', dataset)
922
896
 
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
897
 
929
898
class PieChart2D(PieChart):
930
899
 
966
935
        Chart.__init__(self, *args, **kwargs)
967
936
        self.geo_area = 'world'
968
937
        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
 
        
 
938
 
996
939
    def type_to_url(self):
997
940
        return 'cht=t'
998
941
 
999
942
    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)
 
943
        self.codes = codes
1035
944
 
1036
945
    def get_url_bits(self, data_class=None):
1037
946
        url_bits = Chart.get_url_bits(self, data_class=data_class)
1040
949
            url_bits.append('chld=%s' % ''.join(self.codes))
1041
950
        return url_bits
1042
951
 
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
952
 
1053
953
class GoogleOMeterChart(PieChart):
1054
954
    """Inheriting from PieChart because of similar labeling"""
1077
977
    def data_to_url(self, data_class=None):
1078
978
        if not self.data:
1079
979
            raise NoDataGivenException()
1080
 
        return 'chl=%s' % urllib.parse.quote(self.data[0])
 
980
        return 'chl=%s' % urllib.quote(self.data[0])
1081
981
 
1082
982
    def get_url_bits(self, data_class=None):
1083
983
        url_bits = Chart.get_url_bits(self, data_class=data_class)
1084
984
        if self.encoding:
1085
985
            url_bits.append('choe=%s' % self.encoding)
1086
986
        if self.ec_level:
1087
 
            url_bits.append('chld=%s%%7c%s' % (self.ec_level, self.margin))
 
987
            url_bits.append('chld=%s|%s' % (self.ec_level, self.margin))
1088
988
        return url_bits
1089
989
 
1090
990
    def set_encoding(self, encoding):