/+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-02-08 01:56:15 UTC
  • Revision ID: git-v1:210438f4e203556b9584e8d426b5668c926adcf2
Examples demonstrating last commit added by Trent Mick

Show diffs side-by-side

added added

removed removed

Lines of Context:
84
84
    enc_map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
85
85
 
86
86
    def __repr__(self):
 
87
        max_value = self.max_value()
87
88
        encoded_data = []
88
89
        for data in self.data:
89
90
            sub_data = []
90
91
            for value in data:
91
92
                if value is None:
92
93
                    sub_data.append('_')
93
 
                elif value >= 0 and value <= SimpleData.max_value:
 
94
                elif value >= 0 and value <= max_value:
94
95
                    sub_data.append(SimpleData.enc_map[value])
95
96
                else:
96
 
                    raise DataOutOfRangeException()
 
97
                    raise DataOutOfRangeException('cannot encode value: %d'
 
98
                                                  % value)
97
99
            encoded_data.append(''.join(sub_data))
98
100
        return 'chd=s:' + ','.join(encoded_data)
99
101
 
101
103
    def max_value():
102
104
        return 61
103
105
 
 
106
    @classmethod
 
107
    def scale_value(cls, value, range):
 
108
        lower, upper = range
 
109
        max_value = cls.max_value()
 
110
        scaled = int(round((float(value) - lower) * max_value / upper))
 
111
        clipped = max(0, min(scaled, max_value))
 
112
        return clipped
104
113
 
105
114
class TextData(Data):
106
115
 
107
116
    def __repr__(self):
 
117
        max_value = self.max_value()
108
118
        encoded_data = []
109
119
        for data in self.data:
110
120
            sub_data = []
111
121
            for value in data:
112
122
                if value is None:
113
123
                    sub_data.append(-1)
114
 
                elif value >= 0 and value <= TextData.max_value:
115
 
                    sub_data.append(str(float(value)))
 
124
                elif value >= 0 and value <= max_value:
 
125
                    sub_data.append("%.1f" % float(value))
116
126
                else:
117
127
                    raise DataOutOfRangeException()
118
128
            encoded_data.append(','.join(sub_data))
122
132
    def max_value():
123
133
        return 100
124
134
 
 
135
    @classmethod
 
136
    def scale_value(cls, value, range):
 
137
        lower, upper = range
 
138
        max_value = cls.max_value()
 
139
        scaled = (float(value) - lower) * max_value / upper
 
140
        clipped = max(0, min(scaled, max_value))
 
141
        return clipped
125
142
 
126
143
class ExtendedData(Data):
127
144
    enc_map = \
128
145
        'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-.'
129
146
 
130
147
    def __repr__(self):
 
148
        max_value = self.max_value()
131
149
        encoded_data = []
132
150
        enc_size = len(ExtendedData.enc_map)
133
151
        for data in self.data:
135
153
            for value in data:
136
154
                if value is None:
137
155
                    sub_data.append('__')
138
 
                elif value >= 0 and value <= ExtendedData.max_value:
 
156
                elif value >= 0 and value <= max_value:
139
157
                    first, second = divmod(int(value), enc_size)
140
158
                    sub_data.append('%s%s' % (
141
159
                        ExtendedData.enc_map[first],
151
169
    def max_value():
152
170
        return 4095
153
171
 
 
172
    @classmethod
 
173
    def scale_value(cls, value, range):
 
174
        lower, upper = range
 
175
        max_value = cls.max_value()
 
176
        scaled = int(round((float(value) - lower) * max_value / upper))
 
177
        clipped = max(0, min(scaled, max_value))
 
178
        return clipped
 
179
 
 
180
 
154
181
# Axis Classes
155
182
# -----------------------------------------------------------------------------
156
183
 
237
264
    LINEAR_GRADIENT = 'lg'
238
265
    LINEAR_STRIPES = 'ls'
239
266
 
240
 
    def __init__(self, width, height, title=None, legend=None, colours=None):
 
267
    def __init__(self, width, height, title=None, legend=None, colours=None,
 
268
                 auto_scale=True, x_range=None, y_range=None):
241
269
        assert(type(self) != Chart)  # This is an abstract class
242
270
        assert(isinstance(width, int))
243
271
        assert(isinstance(height, int))
247
275
        self.set_title(title)
248
276
        self.set_legend(legend)
249
277
        self.set_colours(colours)
 
278
 
 
279
        # Data for scaling.
 
280
        self.auto_scale = auto_scale    # Whether to automatically scale data
 
281
        self.x_range = x_range          # (min, max) x-axis range for scaling
 
282
        self.y_range = y_range          # (min, max) y-axis range for scaling
 
283
        self.scaled_data_class = None
 
284
        self.scaled_x_range = None
 
285
        self.scaled_y_range = None
 
286
 
250
287
        self.fill_types = {
251
288
            Chart.BACKGROUND: None,
252
289
            Chart.CHART: None,
384
421
    # -------------------------------------------------------------------------
385
422
 
386
423
    def data_class_detection(self, data):
387
 
        """
388
 
        Detects and returns the data type required based on the range of the
389
 
        data given. The data given must be lists of numbers within a list.
 
424
        """Determines the appropriate data encoding type to give satisfactory
 
425
        resolution (http://code.google.com/apis/chart/#chart_data).
390
426
        """
391
427
        assert(isinstance(data, list) or isinstance(data, tuple))
392
 
        max_value = None
393
 
        for a in data:
394
 
            assert(isinstance(a, list) or isinstance(a, tuple))
395
 
            if max_value is None or max(a) > max_value:
396
 
                max_value = max(a)
397
 
        for data_class in (SimpleData, TextData, ExtendedData):
398
 
            if max_value <= data_class.max_value():
399
 
                return data_class
400
 
        raise DataOutOfRangeException()
 
428
        if not isinstance(self, (LineChart, BarChart, ScatterChart)):
 
429
            # From the link above:
 
430
            #   Simple encoding is suitable for all other types of chart
 
431
            #   regardless of size.
 
432
            return SimpleData
 
433
        elif self.height < 100:
 
434
            # The link above indicates that line and bar charts less
 
435
            # than 300px in size can be suitably represented with the
 
436
            # simple encoding. I've found that this isn't sufficient,
 
437
            # e.g. examples/line-xy-circle.png. Let's try 100px.
 
438
            return SimpleData
 
439
        elif self.height < 500:
 
440
            return TextData
 
441
        else:
 
442
            return ExtendedData
 
443
 
 
444
    def data_x_range(self):
 
445
        """Return a 2-tuple giving the minimum and maximum x-axis 
 
446
        data range.
 
447
        """
 
448
        try:
 
449
            lower = min([min(s) for type, s in self.annotated_data()
 
450
                         if type == 'x'])
 
451
            upper = max([max(s) for type, s in self.annotated_data()
 
452
                         if type == 'x'])
 
453
            return (lower, upper)
 
454
        except ValueError:
 
455
            return None     # no x-axis datasets
 
456
 
 
457
    def data_y_range(self):
 
458
        """Return a 2-tuple giving the minimum and maximum y-axis 
 
459
        data range.
 
460
        """
 
461
        try:
 
462
            lower = min([min(s) for type, s in self.annotated_data()
 
463
                         if type == 'y'])
 
464
            upper = max([max(s) for type, s in self.annotated_data()
 
465
                         if type == 'y'])
 
466
            return (lower, upper)
 
467
        except ValueError:
 
468
            return None     # no y-axis datasets
 
469
 
 
470
    def scaled_data(self, data_class, x_range=None, y_range=None):
 
471
        """Scale `self.data` as appropriate for the given data encoding
 
472
        (data_class) and return it.
 
473
 
 
474
        An optional `y_range` -- a 2-tuple (lower, upper) -- can be
 
475
        given to specify the y-axis bounds. If not given, the range is
 
476
        inferred from the data: (0, <max-value>) presuming no negative
 
477
        values, or (<min-value>, <max-value>) if there are negative
 
478
        values.  `self.scaled_y_range` is set to the actual lower and
 
479
        upper scaling range.
 
480
 
 
481
        Ditto for `x_range`. Note that some chart types don't have x-axis
 
482
        data.
 
483
        """
 
484
        self.scaled_data_class = data_class
 
485
 
 
486
        # Determine the x-axis range for scaling.
 
487
        if x_range is None:
 
488
            x_range = self.data_x_range()
 
489
            if x_range and x_range[0] > 0:
 
490
                x_range = (0, x_range[1])
 
491
        self.scaled_x_range = x_range
 
492
 
 
493
        # Determine the y-axis range for scaling.
 
494
        if y_range is None:
 
495
            y_range = self.data_y_range()
 
496
            if y_range and y_range[0] > 0:
 
497
                y_range = (0, y_range[1])
 
498
        self.scaled_y_range = y_range
 
499
 
 
500
        scaled_data = []
 
501
        for type, dataset in self.annotated_data():
 
502
            if type == 'x':
 
503
                scale_range = x_range
 
504
            elif type == 'y':
 
505
                scale_range = y_range
 
506
            elif type == 'marker-size':
 
507
                scale_range = (0, max(dataset))
 
508
            scaled_data.append([data_class.scale_value(v, scale_range)
 
509
                                for v in dataset])
 
510
        return scaled_data
401
511
 
402
512
    def add_data(self, data):
403
513
        self.data.append(data)
408
518
            data_class = self.data_class_detection(self.data)
409
519
        if not issubclass(data_class, Data):
410
520
            raise UnknownDataTypeException()
411
 
        return repr(data_class(self.data))
 
521
        if self.auto_scale:
 
522
            data = self.scaled_data(data_class, self.x_range, self.y_range)
 
523
        else:
 
524
            data = self.data
 
525
        return repr(data_class(data))
412
526
 
413
527
    # Axis Labels
414
528
    # -------------------------------------------------------------------------
500
614
 
501
615
class ScatterChart(Chart):
502
616
 
503
 
    def __init__(self, *args, **kwargs):
504
 
        Chart.__init__(self, *args, **kwargs)
505
 
 
506
617
    def type_to_url(self):
507
618
        return 'cht=s'
508
619
 
 
620
    def annotated_data(self):
 
621
        yield ('x', self.data[0])
 
622
        yield ('y', self.data[1])
 
623
        if len(self.data) > 2:
 
624
            # The optional third dataset is relative sizing for point
 
625
            # markers.
 
626
            yield ('marker-size', self.data[2])
509
627
 
510
628
class LineChart(Chart):
511
629
 
551
669
    def type_to_url(self):
552
670
        return 'cht=lc'
553
671
 
 
672
    def annotated_data(self):
 
673
        # All datasets are y-axis data.
 
674
        for dataset in self.data:
 
675
            yield ('y', dataset)
554
676
 
555
677
class XYLineChart(LineChart):
556
678
 
557
679
    def type_to_url(self):
558
680
        return 'cht=lxy'
559
681
 
 
682
    def annotated_data(self):
 
683
        # Datasets alternate between x-axis, y-axis.
 
684
        for i, dataset in enumerate(self.data):
 
685
            if i % 2 == 0:
 
686
                yield ('x', dataset)
 
687
            else:
 
688
                yield ('y', dataset)
560
689
 
561
690
class BarChart(Chart):
562
691
 
570
699
 
571
700
    def get_url_bits(self):
572
701
        url_bits = Chart.get_url_bits(self)
573
 
        url_bits.append('chbh=%i' % self.bar_width)
 
702
        if self.bar_width is not None:
 
703
            url_bits.append('chbh=%i' % self.bar_width)
574
704
        return url_bits
575
705
 
576
706
 
579
709
    def type_to_url(self):
580
710
        return 'cht=bhs'
581
711
 
 
712
    def annotated_data(self):
 
713
        for dataset in self.data:
 
714
            yield ('x', dataset)
582
715
 
583
716
class StackedVerticalBarChart(BarChart):
584
717
 
585
718
    def type_to_url(self):
586
719
        return 'cht=bvs'
587
720
 
 
721
    def annotated_data(self):
 
722
        for dataset in self.data:
 
723
            yield ('y', dataset)
 
724
 
588
725
 
589
726
class GroupedBarChart(BarChart):
590
727
 
592
729
        assert(type(self) != GroupedBarChart)  # This is an abstract class
593
730
        BarChart.__init__(self, *args, **kwargs)
594
731
        self.bar_spacing = None
 
732
        self.group_spacing = None
595
733
 
596
734
    def set_bar_spacing(self, spacing):
 
735
        """Set spacing between bars in a group."""
597
736
        self.bar_spacing = spacing
598
737
 
 
738
    def set_group_spacing(self, spacing):
 
739
        """Set spacing between groups of bars."""
 
740
        self.group_spacing = spacing
 
741
 
599
742
    def get_url_bits(self):
600
743
        # Skip 'BarChart.get_url_bits' and call Chart directly so the parent
601
744
        # doesn't add "chbh" before we do.
602
745
        url_bits = Chart.get_url_bits(self)
603
 
        if self.bar_spacing is not None:
604
 
            if self.bar_width is None:
605
 
                raise InvalidParametersException('Bar width is required to ' \
606
 
                    'be set when setting spacing')
 
746
        if self.group_spacing is not None:
 
747
            if self.bar_spacing is None:
 
748
                raise InvalidParametersException('Bar spacing is required to ' \
 
749
                    'be set when setting group spacing')
 
750
            if self.bar_width is None:
 
751
                raise InvalidParametersException('Bar width is required to ' \
 
752
                    'be set when setting bar spacing')
 
753
            url_bits.append('chbh=%i,%i,%i'
 
754
                % (self.bar_width, self.bar_spacing, self.group_spacing))
 
755
        elif self.bar_spacing is not None:
 
756
            if self.bar_width is None:
 
757
                raise InvalidParametersException('Bar width is required to ' \
 
758
                    'be set when setting bar spacing')
607
759
            url_bits.append('chbh=%i,%i' % (self.bar_width, self.bar_spacing))
608
760
        else:
609
761
            url_bits.append('chbh=%i' % self.bar_width)
615
767
    def type_to_url(self):
616
768
        return 'cht=bhg'
617
769
 
 
770
    def annotated_data(self):
 
771
        for dataset in self.data:
 
772
            yield ('x', dataset)
 
773
 
618
774
 
619
775
class GroupedVerticalBarChart(GroupedBarChart):
620
776
 
621
777
    def type_to_url(self):
622
778
        return 'cht=bvg'
623
779
 
 
780
    def annotated_data(self):
 
781
        for dataset in self.data:
 
782
            yield ('y', dataset)
 
783
 
624
784
 
625
785
class PieChart(Chart):
626
786
 
630
790
        self.pie_labels = []
631
791
 
632
792
    def set_pie_labels(self, labels):
633
 
        self.pie_labels = labels
 
793
        self.pie_labels = [urllib.quote(a) for a in labels]
634
794
 
635
795
    def get_url_bits(self):
636
796
        url_bits = Chart.get_url_bits(self)
638
798
            url_bits.append('chl=%s' % '|'.join(self.pie_labels))
639
799
        return url_bits
640
800
 
 
801
    def annotated_data(self):
 
802
        # Datasets are all y-axis data. However, there should only be
 
803
        # one dataset for pie charts.
 
804
        for dataset in self.data:
 
805
            yield ('y', dataset)
 
806
 
641
807
 
642
808
class PieChart2D(PieChart):
643
809
 
656
822
    def type_to_url(self):
657
823
        return 'cht=v'
658
824
 
 
825
    def annotated_data(self):
 
826
        for dataset in self.data:
 
827
            yield ('y', dataset)
 
828
 
659
829
 
660
830
def test():
661
831
    chart = GroupedVerticalBarChart(320, 200)