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