76
95
class Data(object):
78
97
def __init__(self, data):
79
assert(type(self) != Data) # This is an abstract class
98
if type(self) == Data:
99
raise AbstractClassException('This is an abstract class')
83
103
def float_scale_value(cls, value, range):
84
104
lower, upper = range
85
max_value = cls.max_value()
86
scaled = (value-lower) * (float(max_value)/(upper-lower))
105
assert(upper > lower)
106
scaled = (value - lower) * (cls.max_value / (upper - lower))
90
110
def clip_value(cls, value):
91
clipped = max(0, min(value, cls.max_value()))
111
return max(0, min(value, cls.max_value))
95
114
def int_scale_value(cls, value, range):
96
scaled = int(round(cls.float_scale_value(value, range)))
115
return int(round(cls.float_scale_value(value, range)))
100
118
def scale_value(cls, value, range):
101
119
scaled = cls.int_scale_value(value, range)
102
120
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.')
105
131
class SimpleData(Data):
106
134
enc_map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
108
136
def __repr__(self):
109
max_value = self.max_value()
110
137
encoded_data = []
111
138
for data in self.data:
113
140
for value in data:
114
141
if value is None:
115
142
sub_data.append('_')
116
elif value >= 0 and value <= max_value:
143
elif value >= 0 and value <= self.max_value:
117
144
sub_data.append(SimpleData.enc_map[value])
119
146
raise DataOutOfRangeException('cannot encode value: %d'
121
148
encoded_data.append(''.join(sub_data))
122
149
return 'chd=s:' + ','.join(encoded_data)
128
152
class TextData(Data):
130
156
def __repr__(self):
131
max_value = self.max_value()
132
157
encoded_data = []
133
158
for data in self.data:
135
160
for value in data:
136
161
if value is None:
137
162
sub_data.append(-1)
138
elif value >= 0 and value <= max_value:
163
elif value >= 0 and value <= self.max_value:
139
164
sub_data.append("%.1f" % float(value))
141
166
raise DataOutOfRangeException()
142
167
encoded_data.append(','.join(sub_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))
168
return 'chd=t:' + '%7c'.join(encoded_data)
161
171
def scale_value(cls, value, range):
162
172
# use float values instead of integers because we don't need an encode
164
scaled = cls.float_scale_value(value,range)
174
scaled = cls.float_scale_value(value, range)
165
175
clipped = cls.clip_value(scaled)
176
Data.check_clip(scaled, clipped)
168
180
class ExtendedData(Data):
170
184
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-.'
172
186
def __repr__(self):
173
max_value = self.max_value()
174
187
encoded_data = []
175
188
enc_size = len(ExtendedData.enc_map)
176
189
for data in self.data:
284
294
LINEAR_STRIPES = 'ls'
286
296
def __init__(self, width, height, title=None, legend=None, colours=None,
287
auto_scale=True, x_range=None, y_range=None):
288
assert(type(self) != Chart) # This is an abstract class
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')
289
301
assert(isinstance(width, int))
290
302
assert(isinstance(height, int))
291
303
self.width = width
292
304
self.height = height
294
306
self.set_title(title)
307
self.set_title_style(None, None)
295
308
self.set_legend(legend)
309
self.set_legend_position(None)
296
310
self.set_colours(colours)
311
self.set_colours_within_series(colours_within_series)
298
313
# Data 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
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
302
317
self.scaled_data_class = None
303
318
self.scaled_x_range = None
304
319
self.scaled_y_range = None
363
390
# -------------------------------------------------------------------------
365
def download(self, file_name):
366
opener = urllib2.urlopen(self.get_url())
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())
368
398
if opener.headers['content-type'] != 'image/png':
369
399
raise BadContentTypeException('Server responded with a ' \
370
400
'content-type of %s' % opener.headers['content-type'])
372
open(file_name, 'wb').write(urllib.urlopen(self.get_url()).read())
402
open(file_name, 'wb').write(opener.read())
374
404
# Simple settings
375
405
# -------------------------------------------------------------------------
377
407
def set_title(self, title):
379
self.title = urllib.quote(title)
409
self.title = urllib.parse.quote(title)
381
411
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
383
421
def set_legend(self, legend):
384
422
"""legend needs to be a list, tuple or None"""
385
423
assert(isinstance(legend, list) or isinstance(legend, tuple) or
388
self.legend = [urllib.quote(a) for a in legend]
426
self.legend = [urllib.parse.quote(a) for a in legend]
390
428
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
393
437
# -------------------------------------------------------------------------
610
677
url_bits.append('chxt=%s' % ','.join(available_axis))
612
url_bits.append('chxl=%s' % '|'.join(label_axis))
679
url_bits.append('chxl=%s' % '%7c'.join(label_axis))
614
url_bits.append('chxr=%s' % '|'.join(range_axis))
681
url_bits.append('chxr=%s' % '%7c'.join(range_axis))
616
url_bits.append('chxp=%s' % '|'.join(positions))
683
url_bits.append('chxp=%s' % '%7c'.join(positions))
618
url_bits.append('chxs=%s' % '|'.join(styles))
685
url_bits.append('chxs=%s' % '%7c'.join(styles))
619
686
return '&'.join(url_bits)
621
688
# Markers, Ranges and Fill area (chm)
622
689
# -------------------------------------------------------------------------
624
def markers_to_url(self):
625
return 'chm=%s' % '|'.join([','.join(a) for a in self.markers])
691
def markers_to_url(self):
692
return 'chm=%s' % '%7c'.join([','.join(a) for a in self.markers])
627
694
def add_marker(self, index, point, marker_type, colour, size, priority=0):
628
695
self.markers.append((marker_type, colour, str(index), str(point), \
629
696
str(size), str(priority)))
631
698
def add_horizontal_range(self, colour, start, stop):
632
self.markers.append(('r', colour, '1', str(start), str(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)))
634
710
def add_vertical_range(self, colour, start, stop):
635
self.markers.append(('R', colour, '1', str(start), str(stop)))
711
self.markers.append(('R', colour, '0', str(start), str(stop)))
637
713
def add_fill_range(self, colour, index_start, index_end):
638
714
self.markers.append(('b', colour, str(index_start), str(index_end), \
818
897
class PieChart(Chart):
820
899
def __init__(self, *args, **kwargs):
821
assert(type(self) != PieChart) # This is an abstract class
900
if type(self) == PieChart:
901
raise AbstractClassException('This is an abstract class')
822
902
Chart.__init__(self, *args, **kwargs)
823
903
self.pie_labels = []
905
warnings.warn('y_range is not used with %s.' % \
906
(self.__class__.__name__))
825
908
def set_pie_labels(self, labels):
826
self.pie_labels = [urllib.quote(a) for a in labels]
909
self.pie_labels = [urllib.parse.quote(a) for a in labels]
828
911
def get_url_bits(self, data_class=None):
829
912
url_bits = Chart.get_url_bits(self, data_class=data_class)
830
913
if self.pie_labels:
831
url_bits.append('chl=%s' % '|'.join(self.pie_labels))
914
url_bits.append('chl=%s' % '%7c'.join(self.pie_labels))
834
917
def annotated_data(self):
835
918
# Datasets are all y-axis data. However, there should only be
836
919
# one dataset for pie charts.
837
920
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)
841
929
class PieChart2D(PieChart):
877
966
Chart.__init__(self, *args, **kwargs)
878
967
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')
881
996
def type_to_url(self):
884
999
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)
887
1036
def get_url_bits(self, data_class=None):
888
1037
url_bits = Chart.get_url_bits(self, data_class=data_class)
891
1040
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())
895
1053
class GoogleOMeterChart(PieChart):
896
1054
"""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.')
898
1062
def type_to_url(self):
899
1063
return 'cht=gom'
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__':
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)