/+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:
26
26
import math
27
27
import random
28
28
import re
 
29
import warnings
 
30
import copy
29
31
 
30
32
# Helper variables and functions
31
33
# -----------------------------------------------------------------------------
41
43
            'RRGGBB or RRGGBBAA format. One of your colours has %s' % \
42
44
            colour)
43
45
 
 
46
 
 
47
def _reset_warnings():
 
48
    """Helper function to reset all warnings. Used by the unit tests."""
 
49
    globals()['__warningregistry__'] = None
 
50
#def _warn(message):
 
51
#    warnings.warn_explicit(msg, warnings.UserWarning,
 
52
 
 
53
 
44
54
# Exception Classes
45
55
# -----------------------------------------------------------------------------
46
56
 
92
102
    def float_scale_value(cls, value, range):
93
103
        lower, upper = range
94
104
        assert(upper > lower)
95
 
        max_value = cls.max_value()
96
 
        scaled = (value-lower) * (float(max_value) / (upper - lower))
 
105
        scaled = (value - lower) * (float(cls.max_value) / (upper - lower))
97
106
        return scaled
98
107
 
99
108
    @classmethod
100
109
    def clip_value(cls, value):
101
 
        return max(0, min(value, cls.max_value()))
 
110
        return max(0, min(value, cls.max_value))
102
111
 
103
112
    @classmethod
104
113
    def int_scale_value(cls, value, range):
108
117
    def scale_value(cls, value, range):
109
118
        scaled = cls.int_scale_value(value, range)
110
119
        clipped = cls.clip_value(scaled)
 
120
        if clipped != scaled:
 
121
            warnings.warn('One or more of of your data points has been '
 
122
                'clipped because it is out of range.')
111
123
        return clipped
112
124
 
113
125
 
114
126
class SimpleData(Data):
115
127
 
 
128
    max_value = 61
116
129
    enc_map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
117
130
 
118
131
    def __repr__(self):
119
 
        max_value = self.max_value()
120
132
        encoded_data = []
121
133
        for data in self.data:
122
134
            sub_data = []
123
135
            for value in data:
124
136
                if value is None:
125
137
                    sub_data.append('_')
126
 
                elif value >= 0 and value <= max_value:
 
138
                elif value >= 0 and value <= self.max_value:
127
139
                    sub_data.append(SimpleData.enc_map[value])
128
140
                else:
129
141
                    raise DataOutOfRangeException('cannot encode value: %d'
131
143
            encoded_data.append(''.join(sub_data))
132
144
        return 'chd=s:' + ','.join(encoded_data)
133
145
 
134
 
    @staticmethod
135
 
    def max_value():
136
 
        return 61
137
 
 
138
146
 
139
147
class TextData(Data):
140
148
 
 
149
    max_value = 100
 
150
 
141
151
    def __repr__(self):
142
 
        max_value = self.max_value()
143
152
        encoded_data = []
144
153
        for data in self.data:
145
154
            sub_data = []
146
155
            for value in data:
147
156
                if value is None:
148
157
                    sub_data.append(-1)
149
 
                elif value >= 0 and value <= max_value:
 
158
                elif value >= 0 and value <= self.max_value:
150
159
                    sub_data.append("%.1f" % float(value))
151
160
                else:
152
161
                    raise DataOutOfRangeException()
153
162
            encoded_data.append(','.join(sub_data))
154
163
        return 'chd=t:' + '|'.join(encoded_data)
155
164
 
156
 
    @staticmethod
157
 
    def max_value():
158
 
        return 100
159
 
 
160
165
    @classmethod
161
166
    def scale_value(cls, value, range):
162
167
        # use float values instead of integers because we don't need an encode
168
173
 
169
174
class ExtendedData(Data):
170
175
 
 
176
    max_value = 4095
171
177
    enc_map = \
172
178
        'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-.'
173
179
 
174
180
    def __repr__(self):
175
 
        max_value = self.max_value()
176
181
        encoded_data = []
177
182
        enc_size = len(ExtendedData.enc_map)
178
183
        for data in self.data:
180
185
            for value in data:
181
186
                if value is None:
182
187
                    sub_data.append('__')
183
 
                elif value >= 0 and value <= max_value:
 
188
                elif value >= 0 and value <= self.max_value:
184
189
                    first, second = divmod(int(value), enc_size)
185
190
                    sub_data.append('%s%s' % (
186
191
                        ExtendedData.enc_map[first],
192
197
            encoded_data.append(''.join(sub_data))
193
198
        return 'chd=e:' + ','.join(encoded_data)
194
199
 
195
 
    @staticmethod
196
 
    def max_value():
197
 
        return 4095
198
 
 
199
200
 
200
201
# Axis Classes
201
202
# -----------------------------------------------------------------------------
287
288
    LINEAR_STRIPES = 'ls'
288
289
 
289
290
    def __init__(self, width, height, title=None, legend=None, colours=None,
290
 
                 auto_scale=True, x_range=None, y_range=None):
 
291
            auto_scale=True, x_range=None, y_range=None):
291
292
        if type(self) == Chart:
292
293
            raise AbstractClassException('This is an abstract class')
293
294
        assert(isinstance(width, int))
300
301
        self.set_colours(colours)
301
302
 
302
303
        # Data for scaling.
303
 
        self.auto_scale = auto_scale    # Whether to automatically scale data
304
 
        self.x_range = x_range          # (min, max) x-axis range for scaling
305
 
        self.y_range = y_range          # (min, max) y-axis range for scaling
 
304
        self.auto_scale = auto_scale  # Whether to automatically scale data
 
305
        self.x_range = x_range  # (min, max) x-axis range for scaling
 
306
        self.y_range = y_range  # (min, max) y-axis range for scaling
306
307
        self.scaled_data_class = None
307
308
        self.scaled_x_range = None
308
309
        self.scaled_y_range = None
549
550
        if not issubclass(data_class, Data):
550
551
            raise UnknownDataTypeException()
551
552
        if self.auto_scale:
552
 
            print data_class
553
553
            data = self.scaled_data(data_class, self.x_range, self.y_range)
554
554
        else:
555
555
            data = self.data
830
830
            raise AbstractClassException('This is an abstract class')
831
831
        Chart.__init__(self, *args, **kwargs)
832
832
        self.pie_labels = []
 
833
        if self.y_range:
 
834
            warnings.warn('y_range is not used with %s.' % \
 
835
                (self.__class__.__name__))
833
836
 
834
837
    def set_pie_labels(self, labels):
835
838
        self.pie_labels = [urllib.quote(a) for a in labels]
844
847
        # Datasets are all y-axis data. However, there should only be
845
848
        # one dataset for pie charts.
846
849
        for dataset in self.data:
847
 
            yield ('y', dataset)
 
850
            yield ('x', dataset)
848
851
 
849
852
 
850
853
class PieChart2D(PieChart):
905
908
class GoogleOMeterChart(PieChart):
906
909
    """Inheriting from PieChart because of similar labeling"""
907
910
 
 
911
    def __init__(self, *args, **kwargs):
 
912
        PieChart.__init__(self, *args, **kwargs)
 
913
        if self.auto_scale and not self.x_range:
 
914
            warnings.warn('Please specify an x_range with GoogleOMeterChart, '
 
915
                'otherwise one arrow will always be at the max.')
 
916
 
908
917
    def type_to_url(self):
909
918
        return 'cht=gom'
910
919
 
911
920
 
912
921
class ChartGrammar(object):
913
922
 
914
 
    def __init__(self, grammar):
 
923
    def __init__(self):
 
924
        self.grammar = None
 
925
        self.chart = None
 
926
 
 
927
    def parse(self, grammar):
915
928
        self.grammar = grammar
916
929
        self.chart = self.create_chart_instance()
917
930
 
 
931
        for attr in self.grammar:
 
932
            if attr in ('w', 'h', 'type', 'auto_scale', 'x_range', 'y_range'):
 
933
                continue  # These are already parsed in create_chart_instance
 
934
            attr_func = 'parse_' + attr
 
935
            if not hasattr(self, attr_func):
 
936
                warnings.warn('No parser for grammar attribute "%s"' % (attr))
 
937
                continue
 
938
            getattr(self, attr_func)(grammar[attr])
 
939
 
 
940
        return self.chart
 
941
 
 
942
    def parse_data(self, data):
 
943
        self.chart.data = data
 
944
        print self.chart.data
 
945
 
918
946
    @staticmethod
919
947
    def get_possible_chart_types():
920
948
        possible_charts = []
921
 
        for cls_name in globals():
 
949
        for cls_name in globals().keys():
922
950
            if not cls_name.endswith('Chart'):
923
951
                continue
924
952
            cls = globals()[cls_name]
925
953
            # Check if it is an abstract class
926
954
            try:
927
 
                cls(1, 1)
 
955
                a = cls(1, 1, auto_scale=False)
 
956
                del a
928
957
            except AbstractClassException:
929
958
                continue
930
959
            # Strip off "Class"
931
960
            possible_charts.append(cls_name[:-5])
932
961
        return possible_charts
933
962
 
934
 
    def create_chart_instance(self):
 
963
    def create_chart_instance(self, grammar=None):
 
964
        if not grammar:
 
965
            grammar = self.grammar
 
966
        assert(isinstance(grammar, dict))  # grammar must be a dict
935
967
        assert('w' in grammar)  # width is required
936
968
        assert('h' in grammar)  # height is required
937
969
        assert('type' in grammar)  # type is required
 
970
        chart_type = grammar['type']
 
971
        w = grammar['w']
 
972
        h = grammar['h']
 
973
        auto_scale = grammar.get('auto_scale', None)
 
974
        x_range = grammar.get('x_range', None)
 
975
        y_range = grammar.get('y_range', None)
938
976
        types = ChartGrammar.get_possible_chart_types()
939
 
        if grammar['type'] not in types:
 
977
        if chart_type not in types:
940
978
            raise UnknownChartType('%s is an unknown chart type. Possible '
941
 
                'chart types are %s' % (grammar['type'], ','.join(types)))
 
979
                'chart types are %s' % (chart_type, ','.join(types)))
 
980
        return globals()[chart_type + 'Chart'](w, h, auto_scale=auto_scale,
 
981
            x_range=x_range, y_range=y_range)
942
982
 
943
983
    def download(self):
944
984
        pass