95
76
class Data(object):
 
97
78
    def __init__(self, data):
 
98
 
        if type(self) == Data:
 
99
 
            raise AbstractClassException('This is an abstract class')
 
 
79
        assert(type(self) != Data)  # This is an abstract class
 
103
83
    def float_scale_value(cls, value, range):
 
104
84
        lower, upper = range
 
105
 
        assert(upper > lower)
 
106
 
        scaled = (value - lower) * (cls.max_value / (upper - lower))
 
 
85
        max_value = cls.max_value()
 
 
86
        scaled = (value-lower) * (float(max_value)/(upper-lower))
 
110
90
    def clip_value(cls, value):
 
111
 
        return max(0, min(value, cls.max_value))
 
 
91
        clipped = max(0, min(value, cls.max_value()))
 
114
95
    def int_scale_value(cls, value, range):
 
115
 
        return int(round(cls.float_scale_value(value, range)))
 
 
96
        scaled = int(round(cls.float_scale_value(value, range)))
 
118
100
    def scale_value(cls, value, range):
 
119
101
        scaled = cls.int_scale_value(value, range)
 
120
102
        clipped = cls.clip_value(scaled)
 
121
 
        Data.check_clip(scaled, clipped)
 
125
 
    def check_clip(scaled, clipped):
 
126
 
        if clipped != scaled:
 
127
 
            warnings.warn('One or more of of your data points has been '
 
128
 
                'clipped because it is out of range.')
 
131
105
class SimpleData(Data):
 
134
106
    enc_map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
 
136
108
    def __repr__(self):
 
 
109
        max_value = self.max_value()
 
137
110
        encoded_data = []
 
138
111
        for data in self.data:
 
140
113
            for value in data:
 
141
114
                if value is None:
 
142
115
                    sub_data.append('_')
 
143
 
                elif value >= 0 and value <= self.max_value:
 
 
116
                elif value >= 0 and value <= max_value:
 
144
117
                    sub_data.append(SimpleData.enc_map[value])
 
146
119
                    raise DataOutOfRangeException('cannot encode value: %d'
 
 
148
121
            encoded_data.append(''.join(sub_data))
 
149
122
        return 'chd=s:' + ','.join(encoded_data)
 
152
128
class TextData(Data):
 
156
130
    def __repr__(self):
 
 
131
        max_value = self.max_value()
 
157
132
        encoded_data = []
 
158
133
        for data in self.data:
 
160
135
            for value in data:
 
161
136
                if value is None:
 
162
137
                    sub_data.append(-1)
 
163
 
                elif value >= 0 and value <= self.max_value:
 
 
138
                elif value >= 0 and value <= max_value:
 
164
139
                    sub_data.append("%.1f" % float(value))
 
166
141
                    raise DataOutOfRangeException()
 
167
142
            encoded_data.append(','.join(sub_data))
 
168
 
        return 'chd=t:' + '%7c'.join(encoded_data)
 
 
143
        return 'chd=t:' + '|'.join(encoded_data)
 
 
150
    def scale_value(cls, value, range):
 
 
153
            max_value = cls.max_value()
 
 
154
            scaled = (float(value) - lower) * max_value / upper
 
 
155
            clipped = max(0, min(scaled, max_value))
 
171
161
    def scale_value(cls, value, range):
 
172
162
        # use float values instead of integers because we don't need an encode
 
174
 
        scaled = cls.float_scale_value(value, range)
 
 
164
        scaled = cls.float_scale_value(value,range)
 
175
165
        clipped = cls.clip_value(scaled)
 
176
 
        Data.check_clip(scaled, clipped)
 
180
168
class ExtendedData(Data):
 
184
170
        'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-.'
 
186
172
    def __repr__(self):
 
 
173
        max_value = self.max_value()
 
187
174
        encoded_data = []
 
188
175
        enc_size = len(ExtendedData.enc_map)
 
189
176
        for data in self.data:
 
 
294
284
    LINEAR_STRIPES = 'ls'
 
296
286
    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):
 
299
 
        if type(self) == Chart:
 
300
 
            raise AbstractClassException('This is an abstract class')
 
 
287
                 auto_scale=True, x_range=None, y_range=None):
 
 
288
        assert(type(self) != Chart)  # This is an abstract class
 
301
289
        assert(isinstance(width, int))
 
302
290
        assert(isinstance(height, int))
 
303
291
        self.width = width
 
304
292
        self.height = height
 
306
294
        self.set_title(title)
 
307
 
        self.set_title_style(None, None)
 
308
295
        self.set_legend(legend)
 
309
 
        self.set_legend_position(None)
 
310
296
        self.set_colours(colours)
 
311
 
        self.set_colours_within_series(colours_within_series)
 
313
298
        # Data for scaling.
 
314
 
        self.auto_scale = auto_scale  # Whether to automatically scale data
 
315
 
        self.x_range = x_range  # (min, max) x-axis range for scaling
 
316
 
        self.y_range = y_range  # (min, max) y-axis range for scaling
 
 
299
        self.auto_scale = auto_scale    # Whether to automatically scale data
 
 
300
        self.x_range = x_range          # (min, max) x-axis range for scaling
 
 
301
        self.y_range = y_range          # (min, max) y-axis range for scaling
 
317
302
        self.scaled_data_class = None
 
318
303
        self.scaled_x_range = None
 
319
304
        self.scaled_y_range = None
 
 
390
363
    # -------------------------------------------------------------------------
 
392
 
    def download(self, file_name, use_post=True):
 
394
 
            opener = urllib.request.urlopen(self.BASE_URL, self.get_url_extension())
 
396
 
            opener = urllib.request.urlopen(self.get_url())
 
 
365
    def download(self, file_name):
 
 
366
        opener = urllib2.urlopen(self.get_url())
 
398
368
        if opener.headers['content-type'] != 'image/png':
 
399
369
            raise BadContentTypeException('Server responded with a ' \
 
400
370
                'content-type of %s' % opener.headers['content-type'])
 
402
 
        open(file_name, 'wb').write(opener.read())
 
 
372
        open(file_name, 'wb').write(urllib.urlopen(self.get_url()).read())
 
404
374
    # Simple settings
 
405
375
    # -------------------------------------------------------------------------
 
407
377
    def set_title(self, title):
 
409
 
            self.title = urllib.parse.quote(title)
 
 
379
            self.title = urllib.quote(title)
 
411
381
            self.title = None
 
413
 
    def set_title_style(self, colour=None, font_size=None):
 
414
 
        if not colour is None:
 
415
 
            _check_colour(colour)
 
416
 
        if not colour and not font_size:
 
418
 
        self.title_colour = colour or '333333'
 
419
 
        self.title_font_size = font_size or 13.5
 
421
383
    def set_legend(self, legend):
 
422
384
        """legend needs to be a list, tuple or None"""
 
423
385
        assert(isinstance(legend, list) or isinstance(legend, tuple) or
 
426
 
            self.legend = [urllib.parse.quote(a) for a in legend]
 
 
388
            self.legend = [urllib.quote(a) for a in legend]
 
428
390
            self.legend = None
 
430
 
    def set_legend_position(self, legend_position):
 
432
 
            self.legend_position = urllib.parse.quote(legend_position)
 
434
 
            self.legend_position = None
 
437
393
    # -------------------------------------------------------------------------
 
 
677
610
        url_bits.append('chxt=%s' % ','.join(available_axis))
 
679
 
            url_bits.append('chxl=%s' % '%7c'.join(label_axis))
 
 
612
            url_bits.append('chxl=%s' % '|'.join(label_axis))
 
681
 
            url_bits.append('chxr=%s' % '%7c'.join(range_axis))
 
 
614
            url_bits.append('chxr=%s' % '|'.join(range_axis))
 
683
 
            url_bits.append('chxp=%s' % '%7c'.join(positions))
 
 
616
            url_bits.append('chxp=%s' % '|'.join(positions))
 
685
 
            url_bits.append('chxs=%s' % '%7c'.join(styles))
 
 
618
            url_bits.append('chxs=%s' % '|'.join(styles))
 
686
619
        return '&'.join(url_bits)
 
688
621
    # Markers, Ranges and Fill area (chm)
 
689
622
    # -------------------------------------------------------------------------
 
691
 
    def markers_to_url(self):        
 
692
 
        return 'chm=%s' % '%7c'.join([','.join(a) for a in self.markers])
 
 
624
    def markers_to_url(self):
 
 
625
        return 'chm=%s' % '|'.join([','.join(a) for a in self.markers])
 
694
627
    def add_marker(self, index, point, marker_type, colour, size, priority=0):
 
695
628
        self.markers.append((marker_type, colour, str(index), str(point), \
 
696
629
            str(size), str(priority)))
 
698
631
    def add_horizontal_range(self, colour, start, stop):
 
699
 
        self.markers.append(('r', colour, '0', str(start), str(stop)))
 
701
 
    def add_data_line(self, colour, data_set, size, priority=0):
 
702
 
        self.markers.append(('D', colour, str(data_set), '0', str(size), \
 
705
 
    def add_marker_text(self, string, colour, data_set, data_point, size, \
 
707
 
        self.markers.append((str(string), colour, str(data_set), \
 
708
 
            str(data_point), str(size), str(priority)))        
 
 
632
        self.markers.append(('r', colour, '1', str(start), str(stop)))
 
710
634
    def add_vertical_range(self, colour, start, stop):
 
711
 
        self.markers.append(('R', colour, '0', str(start), str(stop)))
 
 
635
        self.markers.append(('R', colour, '1', str(start), str(stop)))
 
713
637
    def add_fill_range(self, colour, index_start, index_end):
 
714
638
        self.markers.append(('b', colour, str(index_start), str(index_end), \
 
 
897
818
class PieChart(Chart):
 
899
820
    def __init__(self, *args, **kwargs):
 
900
 
        if type(self) == PieChart:
 
901
 
            raise AbstractClassException('This is an abstract class')
 
 
821
        assert(type(self) != PieChart)  # This is an abstract class
 
902
822
        Chart.__init__(self, *args, **kwargs)
 
903
823
        self.pie_labels = []
 
905
 
            warnings.warn('y_range is not used with %s.' % \
 
906
 
                (self.__class__.__name__))
 
908
825
    def set_pie_labels(self, labels):
 
909
 
        self.pie_labels = [urllib.parse.quote(a) for a in labels]
 
 
826
        self.pie_labels = [urllib.quote(a) for a in labels]
 
911
828
    def get_url_bits(self, data_class=None):
 
912
829
        url_bits = Chart.get_url_bits(self, data_class=data_class)
 
913
830
        if self.pie_labels:
 
914
 
            url_bits.append('chl=%s' % '%7c'.join(self.pie_labels))
 
 
831
            url_bits.append('chl=%s' % '|'.join(self.pie_labels))
 
917
834
    def annotated_data(self):
 
918
835
        # Datasets are all y-axis data. However, there should only be
 
919
836
        # one dataset for pie charts.
 
920
837
        for dataset in self.data:
 
923
 
    def scaled_data(self, data_class, x_range=None, y_range=None):
 
925
 
            x_range = [0, sum(self.data[0])]
 
926
 
        return Chart.scaled_data(self, data_class, x_range, self.y_range)
 
929
841
class PieChart2D(PieChart):
 
 
966
877
        Chart.__init__(self, *args, **kwargs)
 
967
878
        self.geo_area = 'world'
 
969
 
        self.__areas = ('africa', 'asia', 'europe', 'middle_east',
 
970
 
            'south_america', 'usa', 'world')
 
972
 
            'AD', 'AE', 'AF', 'AG', 'AI', 'AL', 'AM', 'AN', 'AO', 'AQ', 'AR',
 
973
 
            'AS', 'AT', 'AU', 'AW', 'AX', 'AZ', 'BA', 'BB', 'BD', 'BE', 'BF',
 
974
 
            'BG', 'BH', 'BI', 'BJ', 'BL', 'BM', 'BN', 'BO', 'BR', 'BS', 'BT',
 
975
 
            'BV', 'BW', 'BY', 'BZ', 'CA', 'CC', 'CD', 'CF', 'CG', 'CH', 'CI',
 
976
 
            'CK', 'CL', 'CM', 'CN', 'CO', 'CR', 'CU', 'CV', 'CX', 'CY', 'CZ',
 
977
 
            'DE', 'DJ', 'DK', 'DM', 'DO', 'DZ', 'EC', 'EE', 'EG', 'EH', 'ER',
 
978
 
            'ES', 'ET', 'FI', 'FJ', 'FK', 'FM', 'FO', 'FR', 'GA', 'GB', 'GD',
 
979
 
            'GE', 'GF', 'GG', 'GH', 'GI', 'GL', 'GM', 'GN', 'GP', 'GQ', 'GR',
 
980
 
            'GS', 'GT', 'GU', 'GW', 'GY', 'HK', 'HM', 'HN', 'HR', 'HT', 'HU',
 
981
 
            'ID', 'IE', 'IL', 'IM', 'IN', 'IO', 'IQ', 'IR', 'IS', 'IT', 'JE',
 
982
 
            'JM', 'JO', 'JP', 'KE', 'KG', 'KH', 'KI', 'KM', 'KN', 'KP', 'KR',
 
983
 
            'KW', 'KY', 'KZ', 'LA', 'LB', 'LC', 'LI', 'LK', 'LR', 'LS', 'LT',
 
984
 
            'LU', 'LV', 'LY', 'MA', 'MC', 'MD', 'ME', 'MF', 'MG', 'MH', 'MK',
 
985
 
            'ML', 'MM', 'MN', 'MO', 'MP', 'MQ', 'MR', 'MS', 'MT', 'MU', 'MV',
 
986
 
            'MW', 'MX', 'MY', 'MZ', 'NA', 'NC', 'NE', 'NF', 'NG', 'NI', 'NL',
 
987
 
            'NO', 'NP', 'NR', 'NU', 'NZ', 'OM', 'PA', 'PE', 'PF', 'PG', 'PH',
 
988
 
            'PK', 'PL', 'PM', 'PN', 'PR', 'PS', 'PT', 'PW', 'PY', 'QA', 'RE',
 
989
 
            'RO', 'RS', 'RU', 'RW', 'SA', 'SB', 'SC', 'SD', 'SE', 'SG', 'SH',
 
990
 
            'SI', 'SJ', 'SK', 'SL', 'SM', 'SN', 'SO', 'SR', 'ST', 'SV', 'SY',
 
991
 
            'SZ', 'TC', 'TD', 'TF', 'TG', 'TH', 'TJ', 'TK', 'TL', 'TM', 'TN',
 
992
 
            'TO', 'TR', 'TT', 'TV', 'TW', 'TZ', 'UA', 'UG', 'UM', 'US', 'UY',
 
993
 
            'UZ', 'VA', 'VC', 'VE', 'VG', 'VI', 'VN', 'VU', 'WF', 'WS', 'YE',
 
994
 
            'YT', 'ZA', 'ZM', 'ZW')
 
996
881
    def type_to_url(self):
 
999
884
    def set_codes(self, codes):
 
1000
 
        '''Set the country code map for the data.
 
1001
 
        Codes given in a list.
 
1012
 
            if cc in self.__ccodes:
 
1015
 
                raise UnknownCountryCodeException(cc)
 
1017
 
        self.codes = codemap
 
1019
 
    def set_geo_area(self, area):
 
1020
 
        '''Sets the geo area for the map.
 
1031
 
        if area in self.__areas:
 
1032
 
            self.geo_area = area
 
1034
 
            raise UnknownChartType('Unknown chart type for maps: %s' %area)
 
1036
887
    def get_url_bits(self, data_class=None):
 
1037
888
        url_bits = Chart.get_url_bits(self, data_class=data_class)
 
 
1040
891
            url_bits.append('chld=%s' % ''.join(self.codes))
 
1043
 
    def add_data_dict(self, datadict):
 
1044
 
        '''Sets the data and country codes via a dictionary.
 
1046
 
        i.e. {'DE': 50, 'GB': 30, 'AT': 70}
 
1049
 
        self.set_codes(datadict.keys())
 
1050
 
        self.add_data(datadict.values())
 
1053
895
class GoogleOMeterChart(PieChart):
 
1054
896
    """Inheriting from PieChart because of similar labeling"""
 
1056
 
    def __init__(self, *args, **kwargs):
 
1057
 
        PieChart.__init__(self, *args, **kwargs)
 
1058
 
        if self.auto_scale and not self.x_range:
 
1059
 
            warnings.warn('Please specify an x_range with GoogleOMeterChart, '
 
1060
 
                'otherwise one arrow will always be at the max.')
 
1062
898
    def type_to_url(self):
 
1063
899
        return 'cht=gom'
 
1066
 
class QRChart(Chart):
 
1068
 
    def __init__(self, *args, **kwargs):
 
1069
 
        Chart.__init__(self, *args, **kwargs)
 
1070
 
        self.encoding = None
 
1071
 
        self.ec_level = None
 
1074
 
    def type_to_url(self):
 
1077
 
    def data_to_url(self, data_class=None):
 
1079
 
            raise NoDataGivenException()
 
1080
 
        return 'chl=%s' % urllib.parse.quote(self.data[0])
 
1082
 
    def get_url_bits(self, data_class=None):
 
1083
 
        url_bits = Chart.get_url_bits(self, data_class=data_class)
 
1085
 
            url_bits.append('choe=%s' % self.encoding)
 
1087
 
            url_bits.append('chld=%s%%7c%s' % (self.ec_level, self.margin))
 
1090
 
    def set_encoding(self, encoding):
 
1091
 
        self.encoding = encoding
 
1093
 
    def set_ec(self, level, margin):
 
1094
 
        self.ec_level = level
 
1095
 
        self.margin = margin
 
1098
 
class ChartGrammar(object):
 
1104
 
    def parse(self, grammar):
 
1105
 
        self.grammar = grammar
 
1106
 
        self.chart = self.create_chart_instance()
 
1108
 
        for attr in self.grammar:
 
1109
 
            if attr in ('w', 'h', 'type', 'auto_scale', 'x_range', 'y_range'):
 
1110
 
                continue  # These are already parsed in create_chart_instance
 
1111
 
            attr_func = 'parse_' + attr
 
1112
 
            if not hasattr(self, attr_func):
 
1113
 
                warnings.warn('No parser for grammar attribute "%s"' % (attr))
 
1115
 
            getattr(self, attr_func)(grammar[attr])
 
1119
 
    def parse_data(self, data):
 
1120
 
        self.chart.data = data
 
1123
 
    def get_possible_chart_types():
 
1124
 
        possible_charts = []
 
1125
 
        for cls_name in globals().keys():
 
1126
 
            if not cls_name.endswith('Chart'):
 
1128
 
            cls = globals()[cls_name]
 
1129
 
            # Check if it is an abstract class
 
1131
 
                a = cls(1, 1, auto_scale=False)
 
1133
 
            except AbstractClassException:
 
1136
 
            possible_charts.append(cls_name[:-5])
 
1137
 
        return possible_charts
 
1139
 
    def create_chart_instance(self, grammar=None):
 
1141
 
            grammar = self.grammar
 
1142
 
        assert(isinstance(grammar, dict))  # grammar must be a dict
 
1143
 
        assert('w' in grammar)  # width is required
 
1144
 
        assert('h' in grammar)  # height is required
 
1145
 
        assert('type' in grammar)  # type is required
 
1146
 
        chart_type = grammar['type']
 
1149
 
        auto_scale = grammar.get('auto_scale', None)
 
1150
 
        x_range = grammar.get('x_range', None)
 
1151
 
        y_range = grammar.get('y_range', None)
 
1152
 
        types = ChartGrammar.get_possible_chart_types()
 
1153
 
        if chart_type not in types:
 
1154
 
            raise UnknownChartType('%s is an unknown chart type. Possible '
 
1155
 
                'chart types are %s' % (chart_type, ','.join(types)))
 
1156
 
        return globals()[chart_type + 'Chart'](w, h, auto_scale=auto_scale,
 
1157
 
            x_range=x_range, y_range=y_range)
 
 
903
    chart = PieChart2D(320, 200)
 
 
904
    chart = ScatterChart(320, 200)
 
 
905
    chart = SimpleLineChart(320, 200)
 
 
906
    chart = GroupedVerticalBarChart(320, 200)
 
 
907
#    chart = SplineRadarChart(500, 500)
 
 
908
#    chart = MapChart(440, 220)
 
 
909
#    chart = GoogleOMeterChart(440, 220, x_range=(0, 100))
 
 
910
    sine_data = [math.sin(float(a) / math.pi) * 100 for a in xrange(100)]
 
 
911
    random_data = [random.random() * 100 for a in xrange(100)]
 
 
912
    random_data2 = [random.random() * 50 for a in xrange(100)]
 
 
913
#    chart.set_bar_width(50)
 
 
914
#    chart.set_bar_spacing(0)
 
 
915
    chart.add_data(sine_data)
 
 
916
    chart.add_data(random_data)
 
 
917
    chart.set_zero_line(1, .5)
 
 
918
#    chart.add_data(random_data2)
 
 
919
#    chart.set_line_style(0, thickness=5)
 
 
920
#    chart.set_line_style(1, thickness=2, line_segment=10, blank_segment=5)
 
 
921
#    chart.set_title('heloooo weeee')
 
 
922
#    chart.set_legend(('sine wave', 'random * x'))
 
 
923
    chart.set_colours(('ee2000', 'DDDDAA', 'fF03f2'))
 
 
924
#    chart.fill_solid(Chart.ALPHA, '123456')
 
 
925
#    chart.fill_linear_gradient(Chart.ALPHA, 20, '004070', 1, '300040', 0,
 
 
927
#    chart.fill_linear_stripes(Chart.CHART, 20, '204070', .2, '300040', .2,
 
 
929
#    axis_left_index = chart.set_axis_range(Axis.LEFT, 0, 10)
 
 
930
#    axis_right_index = chart.set_axis_range(Axis.RIGHT, 5, 30)
 
 
931
#    axis_bottom_index = chart.set_axis_labels(Axis.BOTTOM, [1, 25, 95])
 
 
932
#    chart.set_axis_positions(axis_bottom_index, [1, 25, 95])
 
 
933
#    chart.set_axis_style(axis_bottom_index, '003050', 15)
 
 
935
#    chart.set_pie_labels(('apples', 'oranges', 'bananas'))
 
 
937
#    chart.set_grid(10, 10)
 
 
938
#    for a in xrange(0, 100, 10):
 
 
939
#        chart.add_marker(1, a, 'a', 'AACA20', 10)
 
 
941
#    chart.add_horizontal_range('00A020', .2, .5)
 
 
942
#    chart.add_vertical_range('00c030', .2, .4)
 
 
944
#    chart.add_fill_simple('303030A0')
 
 
946
#    chart.set_codes(['AU', 'AT', 'US'])
 
 
947
#    chart.add_data([1,2,3])
 
 
948
#    chart.set_colours(('EEEEEE', '000000', '00FF00'))
 
 
950
#    chart.add_data([50,75])
 
 
951
#    chart.set_pie_labels(('apples', 'oranges'))
 
 
953
    url = chart.get_url()
 
 
956
    chart.download('test.png')
 
 
959
        data = urllib.urlopen(chart.get_url()).read()
 
 
960
        open('meh.png', 'wb').write(data)
 
 
961
        os.system('eog meh.png')
 
 
964
if __name__ == '__main__':