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