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
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), \
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.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__':