/+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 07:34:55 UTC
  • Revision ID: git-v1:2ce2bd2a423b1305ad2ad81c72b69c004fecec05
Added some example helpers for MapChart

Show diffs side-by-side

added added

removed removed

Lines of Context:
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.2.2'
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
102
101
    def float_scale_value(cls, value, range):
103
102
        lower, upper = range
104
103
        assert(upper > lower)
105
 
        scaled = (value - lower) * (float(cls.max_value) / (upper - lower))
 
104
        scaled = (value - lower) * (cls.max_value / (upper - lower))
106
105
        return scaled
107
106
 
108
107
    @classmethod
117
116
    def scale_value(cls, value, range):
118
117
        scaled = cls.int_scale_value(value, range)
119
118
        clipped = cls.clip_value(scaled)
 
119
        Data.check_clip(scaled, clipped)
 
120
        return clipped
 
121
 
 
122
    @staticmethod
 
123
    def check_clip(scaled, clipped):
120
124
        if clipped != scaled:
121
125
            warnings.warn('One or more of of your data points has been '
122
126
                'clipped because it is out of range.')
123
 
        return clipped
124
127
 
125
128
 
126
129
class SimpleData(Data):
160
163
                else:
161
164
                    raise DataOutOfRangeException()
162
165
            encoded_data.append(','.join(sub_data))
163
 
        return 'chd=t:' + '|'.join(encoded_data)
 
166
        return 'chd=t:' + '%7c'.join(encoded_data)
164
167
 
165
168
    @classmethod
166
169
    def scale_value(cls, value, range):
168
171
        # map index
169
172
        scaled = cls.float_scale_value(value, range)
170
173
        clipped = cls.clip_value(scaled)
 
174
        Data.check_clip(scaled, clipped)
171
175
        return clipped
172
176
 
173
177
 
254
258
        self.values = [str(a) for a in values]
255
259
 
256
260
    def __repr__(self):
257
 
        return '%i:|%s' % (self.axis_index, '|'.join(self.values))
 
261
        return '%i:%%7c%s' % (self.axis_index, '%7c'.join(self.values))
258
262
 
259
263
 
260
264
class RangeAxis(Axis):
288
292
    LINEAR_STRIPES = 'ls'
289
293
 
290
294
    def __init__(self, width, height, title=None, legend=None, colours=None,
291
 
            auto_scale=True, x_range=None, y_range=None):
 
295
            auto_scale=True, x_range=None, y_range=None,
 
296
            colours_within_series=None):
292
297
        if type(self) == Chart:
293
298
            raise AbstractClassException('This is an abstract class')
294
299
        assert(isinstance(width, int))
297
302
        self.height = height
298
303
        self.data = []
299
304
        self.set_title(title)
 
305
        self.set_title_style(None, None)
300
306
        self.set_legend(legend)
 
307
        self.set_legend_position(None)
301
308
        self.set_colours(colours)
 
309
        self.set_colours_within_series(colours_within_series)
302
310
 
303
311
        # Data for scaling.
304
312
        self.auto_scale = auto_scale  # Whether to automatically scale data
339
347
        # optional arguments
340
348
        if self.title:
341
349
            url_bits.append('chtt=%s' % self.title)
 
350
        if self.title_colour and self.title_font_size:
 
351
            url_bits.append('chts=%s,%s' % (self.title_colour, \
 
352
                self.title_font_size))
342
353
        if self.legend:
343
 
            url_bits.append('chdl=%s' % '|'.join(self.legend))
 
354
            url_bits.append('chdl=%s' % '%7c'.join(self.legend))
 
355
        if self.legend_position:
 
356
            url_bits.append('chdlp=%s' % (self.legend_position))
344
357
        if self.colours:
345
 
            url_bits.append('chco=%s' % ','.join(self.colours))
 
358
            url_bits.append('chco=%s' % ','.join(self.colours))            
 
359
        if self.colours_within_series:
 
360
            url_bits.append('chco=%s' % '%7c'.join(self.colours_within_series))
346
361
        ret = self.fill_to_url()
347
362
        if ret:
348
363
            url_bits.append(ret)
349
364
        ret = self.axis_to_url()
350
365
        if ret:
351
 
            url_bits.append(ret)
 
366
            url_bits.append(ret)                    
352
367
        if self.markers:
353
 
            url_bits.append(self.markers_to_url())
 
368
            url_bits.append(self.markers_to_url())        
354
369
        if self.line_styles:
355
370
            style = []
356
371
            for index in xrange(max(self.line_styles) + 1):
359
374
                else:
360
375
                    values = ('1', )
361
376
                style.append(','.join(values))
362
 
            url_bits.append('chls=%s' % '|'.join(style))
 
377
            url_bits.append('chls=%s' % '%7c'.join(style))
363
378
        if self.grid:
364
379
            url_bits.append('chg=%s' % self.grid)
365
380
        return url_bits
374
389
            raise BadContentTypeException('Server responded with a ' \
375
390
                'content-type of %s' % opener.headers['content-type'])
376
391
 
377
 
        open(file_name, 'wb').write(urllib.urlopen(self.get_url()).read())
 
392
        open(file_name, 'wb').write(opener.read())
378
393
 
379
394
    # Simple settings
380
395
    # -------------------------------------------------------------------------
385
400
        else:
386
401
            self.title = None
387
402
 
 
403
    def set_title_style(self, colour, font_size):
 
404
        if not colour is None:
 
405
            _check_colour(colour)
 
406
        self.title_colour = colour
 
407
        self.title_font_size = font_size
 
408
 
388
409
    def set_legend(self, legend):
389
410
        """legend needs to be a list, tuple or None"""
390
411
        assert(isinstance(legend, list) or isinstance(legend, tuple) or
394
415
        else:
395
416
            self.legend = None
396
417
 
 
418
    def set_legend_position(self, legend_position):
 
419
        if legend_position:
 
420
            self.legend_position = urllib.quote(legend_position)
 
421
        else:    
 
422
            self.legend_position = None
 
423
 
397
424
    # Chart colours
398
425
    # -------------------------------------------------------------------------
399
426
 
407
434
                _check_colour(col)
408
435
        self.colours = colours
409
436
 
 
437
    def set_colours_within_series(self, colours):
 
438
        # colours needs to be a list, tuple or None
 
439
        assert(isinstance(colours, list) or isinstance(colours, tuple) or
 
440
            colours is None)
 
441
        # make sure the colours are in the right format
 
442
        if colours:
 
443
            for col in colours:
 
444
                _check_colour(col)
 
445
        self.colours_within_series = colours        
 
446
 
410
447
    # Background/Chart colours
411
448
    # -------------------------------------------------------------------------
412
449
 
421
458
        assert(angle >= 0 and angle <= 90)
422
459
        assert(len(args) % 2 == 0)
423
460
        args = list(args)  # args is probably a tuple and we need to mutate
424
 
        for a in xrange(len(args) / 2):
 
461
        for a in xrange(int(len(args) / 2)):
425
462
            col = args[a * 2]
426
463
            offset = args[a * 2 + 1]
427
464
            _check_colour(col)
448
485
                areas.append('%s,%s,%s' % (area, self.fill_types[area], \
449
486
                    self.fill_area[area]))
450
487
        if areas:
451
 
            return 'chf=' + '|'.join(areas)
 
488
            return 'chf=' + '%7c'.join(areas)
452
489
 
453
490
    # Data
454
491
    # -------------------------------------------------------------------------
472
509
        else:
473
510
            return ExtendedData
474
511
 
 
512
    def _filter_none(self, data):
 
513
        return [r for r in data if r is not None]
 
514
 
475
515
    def data_x_range(self):
476
516
        """Return a 2-tuple giving the minimum and maximum x-axis
477
517
        data range.
478
518
        """
479
519
        try:
480
 
            lower = min([min(s) for type, s in self.annotated_data()
 
520
            lower = min([min(self._filter_none(s))
 
521
                         for type, s in self.annotated_data()
481
522
                         if type == 'x'])
482
 
            upper = max([max(s) for type, s in self.annotated_data()
 
523
            upper = max([max(self._filter_none(s))
 
524
                         for type, s in self.annotated_data()
483
525
                         if type == 'x'])
484
526
            return (lower, upper)
485
527
        except ValueError:
490
532
        data range.
491
533
        """
492
534
        try:
493
 
            lower = min([min(s) for type, s in self.annotated_data()
 
535
            lower = min([min(self._filter_none(s))
 
536
                         for type, s in self.annotated_data()
494
537
                         if type == 'y'])
495
 
            upper = max([max(s) + 1 for type, s in self.annotated_data()
 
538
            upper = max([max(self._filter_none(s)) + 1
 
539
                         for type, s in self.annotated_data()
496
540
                         if type == 'y'])
497
541
            return (lower, upper)
498
542
        except ValueError:
518
562
        if x_range is None:
519
563
            x_range = self.data_x_range()
520
564
            if x_range and x_range[0] > 0:
521
 
                x_range = (0, x_range[1])
 
565
                x_range = (x_range[0], x_range[1])
522
566
        self.scaled_x_range = x_range
523
567
 
524
568
        # Determine the y-axis range for scaling.
525
569
        if y_range is None:
526
570
            y_range = self.data_y_range()
527
571
            if y_range and y_range[0] > 0:
528
 
                y_range = (0, y_range[1])
 
572
                y_range = (y_range[0], y_range[1])
529
573
        self.scaled_y_range = y_range
530
574
 
531
575
        scaled_data = []
536
580
                scale_range = y_range
537
581
            elif type == 'marker-size':
538
582
                scale_range = (0, max(dataset))
539
 
            scaled_data.append([data_class.scale_value(v, scale_range)
540
 
                                for v in dataset])
 
583
            scaled_dataset = []
 
584
            for v in dataset:
 
585
                if v is None:
 
586
                    scaled_dataset.append(None)
 
587
                else:
 
588
                    scaled_dataset.append(
 
589
                        data_class.scale_value(v, scale_range))
 
590
            scaled_data.append(scaled_dataset)
541
591
        return scaled_data
542
592
 
543
593
    def add_data(self, data):
564
614
 
565
615
    def set_axis_labels(self, axis_type, values):
566
616
        assert(axis_type in Axis.TYPES)
567
 
        values = [urllib.quote(a) for a in values]
 
617
        values = [urllib.quote(str(a)) for a in values]
568
618
        axis_index = len(self.axis)
569
619
        axis = LabelAxis(axis_index, axis_type, values)
570
620
        self.axis.append(axis)
614
664
        url_bits = []
615
665
        url_bits.append('chxt=%s' % ','.join(available_axis))
616
666
        if label_axis:
617
 
            url_bits.append('chxl=%s' % '|'.join(label_axis))
 
667
            url_bits.append('chxl=%s' % '%7c'.join(label_axis))
618
668
        if range_axis:
619
 
            url_bits.append('chxr=%s' % '|'.join(range_axis))
 
669
            url_bits.append('chxr=%s' % '%7c'.join(range_axis))
620
670
        if positions:
621
 
            url_bits.append('chxp=%s' % '|'.join(positions))
 
671
            url_bits.append('chxp=%s' % '%7c'.join(positions))
622
672
        if styles:
623
 
            url_bits.append('chxs=%s' % '|'.join(styles))
 
673
            url_bits.append('chxs=%s' % '%7c'.join(styles))
624
674
        return '&'.join(url_bits)
625
675
 
626
676
    # Markers, Ranges and Fill area (chm)
627
677
    # -------------------------------------------------------------------------
628
678
 
629
 
    def markers_to_url(self):
630
 
        return 'chm=%s' % '|'.join([','.join(a) for a in self.markers])
 
679
    def markers_to_url(self):        
 
680
        return 'chm=%s' % '%7c'.join([','.join(a) for a in self.markers])
631
681
 
632
682
    def add_marker(self, index, point, marker_type, colour, size, priority=0):
633
683
        self.markers.append((marker_type, colour, str(index), str(point), \
634
684
            str(size), str(priority)))
635
685
 
636
686
    def add_horizontal_range(self, colour, start, stop):
637
 
        self.markers.append(('r', colour, '1', str(start), str(stop)))
 
687
        self.markers.append(('r', colour, '0', str(start), str(stop)))
 
688
 
 
689
    def add_data_line(self, colour, data_set, size, priority=0):
 
690
        self.markers.append(('D', colour, str(data_set), '0', str(size), \
 
691
            str(priority)))
 
692
 
 
693
    def add_marker_text(self, string, colour, data_set, data_point, size, \
 
694
            priority=0):
 
695
        self.markers.append((str(string), colour, str(data_set), \
 
696
            str(data_point), str(size), str(priority)))        
638
697
 
639
698
    def add_vertical_range(self, colour, start, stop):
640
 
        self.markers.append(('R', colour, '1', str(start), str(stop)))
 
699
        self.markers.append(('R', colour, '0', str(start), str(stop)))
641
700
 
642
701
    def add_fill_range(self, colour, index_start, index_end):
643
702
        self.markers.append(('b', colour, str(index_start), str(index_end), \
840
899
    def get_url_bits(self, data_class=None):
841
900
        url_bits = Chart.get_url_bits(self, data_class=data_class)
842
901
        if self.pie_labels:
843
 
            url_bits.append('chl=%s' % '|'.join(self.pie_labels))
 
902
            url_bits.append('chl=%s' % '%7c'.join(self.pie_labels))
844
903
        return url_bits
845
904
 
846
905
    def annotated_data(self):
849
908
        for dataset in self.data:
850
909
            yield ('x', dataset)
851
910
 
 
911
    def scaled_data(self, data_class, x_range=None, y_range=None):
 
912
        if not x_range:
 
913
            x_range = [0, sum(self.data[0])]
 
914
        return Chart.scaled_data(self, data_class, x_range, self.y_range)
 
915
 
852
916
 
853
917
class PieChart2D(PieChart):
854
918
 
918
982
        return 'cht=gom'
919
983
 
920
984
 
 
985
class QRChart(Chart):
 
986
 
 
987
    def __init__(self, *args, **kwargs):
 
988
        Chart.__init__(self, *args, **kwargs)
 
989
        self.encoding = None
 
990
        self.ec_level = None
 
991
        self.margin = None
 
992
 
 
993
    def type_to_url(self):
 
994
        return 'cht=qr'
 
995
 
 
996
    def data_to_url(self, data_class=None):
 
997
        if not self.data:
 
998
            raise NoDataGivenException()
 
999
        return 'chl=%s' % urllib.quote(self.data[0])
 
1000
 
 
1001
    def get_url_bits(self, data_class=None):
 
1002
        url_bits = Chart.get_url_bits(self, data_class=data_class)
 
1003
        if self.encoding:
 
1004
            url_bits.append('choe=%s' % self.encoding)
 
1005
        if self.ec_level:
 
1006
            url_bits.append('chld=%s%%7c%s' % (self.ec_level, self.margin))
 
1007
        return url_bits
 
1008
 
 
1009
    def set_encoding(self, encoding):
 
1010
        self.encoding = encoding
 
1011
 
 
1012
    def set_ec(self, level, margin):
 
1013
        self.ec_level = level
 
1014
        self.margin = margin
 
1015
 
 
1016
 
921
1017
class ChartGrammar(object):
922
1018
 
923
1019
    def __init__(self):
941
1037
 
942
1038
    def parse_data(self, data):
943
1039
        self.chart.data = data
944
 
        print self.chart.data
945
1040
 
946
1041
    @staticmethod
947
1042
    def get_possible_chart_types():