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
610
671
url_bits.append('chxt=%s' % ','.join(available_axis))
612
url_bits.append('chxl=%s' % '|'.join(label_axis))
673
url_bits.append('chxl=%s' % '%7c'.join(label_axis))
614
url_bits.append('chxr=%s' % '|'.join(range_axis))
675
url_bits.append('chxr=%s' % '%7c'.join(range_axis))
616
url_bits.append('chxp=%s' % '|'.join(positions))
677
url_bits.append('chxp=%s' % '%7c'.join(positions))
618
url_bits.append('chxs=%s' % '|'.join(styles))
679
url_bits.append('chxs=%s' % '%7c'.join(styles))
619
680
return '&'.join(url_bits)
621
682
# Markers, Ranges and Fill area (chm)
622
683
# -------------------------------------------------------------------------
624
def markers_to_url(self):
625
return 'chm=%s' % '|'.join([','.join(a) for a in self.markers])
685
def markers_to_url(self):
686
return 'chm=%s' % '%7c'.join([','.join(a) for a in self.markers])
627
688
def add_marker(self, index, point, marker_type, colour, size, priority=0):
628
689
self.markers.append((marker_type, colour, str(index), str(point), \
629
690
str(size), str(priority)))
631
692
def add_horizontal_range(self, colour, start, stop):
632
self.markers.append(('r', colour, '1', str(start), str(stop)))
693
self.markers.append(('r', colour, '0', str(start), str(stop)))
695
def add_data_line(self, colour, data_set, size, priority=0):
696
self.markers.append(('D', colour, str(data_set), '0', str(size), \
699
def add_marker_text(self, string, colour, data_set, data_point, size, \
701
self.markers.append((str(string), colour, str(data_set), \
702
str(data_point), str(size), str(priority)))
634
704
def add_vertical_range(self, colour, start, stop):
635
self.markers.append(('R', colour, '1', str(start), str(stop)))
705
self.markers.append(('R', colour, '0', str(start), str(stop)))
637
707
def add_fill_range(self, colour, index_start, index_end):
638
708
self.markers.append(('b', colour, str(index_start), str(index_end), \
877
960
Chart.__init__(self, *args, **kwargs)
878
961
self.geo_area = 'world'
963
self.__areas = ('africa', 'asia', 'europe', 'middle_east',
964
'south_america', 'usa', 'world')
966
'AD', 'AE', 'AF', 'AG', 'AI', 'AL', 'AM', 'AN', 'AO', 'AQ', 'AR',
967
'AS', 'AT', 'AU', 'AW', 'AX', 'AZ', 'BA', 'BB', 'BD', 'BE', 'BF',
968
'BG', 'BH', 'BI', 'BJ', 'BL', 'BM', 'BN', 'BO', 'BR', 'BS', 'BT',
969
'BV', 'BW', 'BY', 'BZ', 'CA', 'CC', 'CD', 'CF', 'CG', 'CH', 'CI',
970
'CK', 'CL', 'CM', 'CN', 'CO', 'CR', 'CU', 'CV', 'CX', 'CY', 'CZ',
971
'DE', 'DJ', 'DK', 'DM', 'DO', 'DZ', 'EC', 'EE', 'EG', 'EH', 'ER',
972
'ES', 'ET', 'FI', 'FJ', 'FK', 'FM', 'FO', 'FR', 'GA', 'GB', 'GD',
973
'GE', 'GF', 'GG', 'GH', 'GI', 'GL', 'GM', 'GN', 'GP', 'GQ', 'GR',
974
'GS', 'GT', 'GU', 'GW', 'GY', 'HK', 'HM', 'HN', 'HR', 'HT', 'HU',
975
'ID', 'IE', 'IL', 'IM', 'IN', 'IO', 'IQ', 'IR', 'IS', 'IT', 'JE',
976
'JM', 'JO', 'JP', 'KE', 'KG', 'KH', 'KI', 'KM', 'KN', 'KP', 'KR',
977
'KW', 'KY', 'KZ', 'LA', 'LB', 'LC', 'LI', 'LK', 'LR', 'LS', 'LT',
978
'LU', 'LV', 'LY', 'MA', 'MC', 'MD', 'ME', 'MF', 'MG', 'MH', 'MK',
979
'ML', 'MM', 'MN', 'MO', 'MP', 'MQ', 'MR', 'MS', 'MT', 'MU', 'MV',
980
'MW', 'MX', 'MY', 'MZ', 'NA', 'NC', 'NE', 'NF', 'NG', 'NI', 'NL',
981
'NO', 'NP', 'NR', 'NU', 'NZ', 'OM', 'PA', 'PE', 'PF', 'PG', 'PH',
982
'PK', 'PL', 'PM', 'PN', 'PR', 'PS', 'PT', 'PW', 'PY', 'QA', 'RE',
983
'RO', 'RS', 'RU', 'RW', 'SA', 'SB', 'SC', 'SD', 'SE', 'SG', 'SH',
984
'SI', 'SJ', 'SK', 'SL', 'SM', 'SN', 'SO', 'SR', 'ST', 'SV', 'SY',
985
'SZ', 'TC', 'TD', 'TF', 'TG', 'TH', 'TJ', 'TK', 'TL', 'TM', 'TN',
986
'TO', 'TR', 'TT', 'TV', 'TW', 'TZ', 'UA', 'UG', 'UM', 'US', 'UY',
987
'UZ', 'VA', 'VC', 'VE', 'VG', 'VI', 'VN', 'VU', 'WF', 'WS', 'YE',
988
'YT', 'ZA', 'ZM', 'ZW')
881
990
def type_to_url(self):
884
993
def set_codes(self, codes):
994
'''Set the country code map for the data.
995
Codes given in a list.
1006
if cc in self.__ccodes:
1009
raise UnknownCountryCodeException(cc)
1011
self.codes = codemap
1013
def set_geo_area(self, area):
1014
'''Sets the geo area for the map.
1025
if area in self.__areas:
1026
self.geo_area = area
1028
raise UnknownChartType('Unknown chart type for maps: %s' %area)
887
1030
def get_url_bits(self, data_class=None):
888
1031
url_bits = Chart.get_url_bits(self, data_class=data_class)
891
1034
url_bits.append('chld=%s' % ''.join(self.codes))
1037
def add_data_dict(self, datadict):
1038
'''Sets the data and country codes via a dictionary.
1040
i.e. {'DE': 50, 'GB': 30, 'AT': 70}
1043
self.set_codes(datadict.keys())
1044
self.add_data(datadict.values())
895
1047
class GoogleOMeterChart(PieChart):
896
1048
"""Inheriting from PieChart because of similar labeling"""
1050
def __init__(self, *args, **kwargs):
1051
PieChart.__init__(self, *args, **kwargs)
1052
if self.auto_scale and not self.x_range:
1053
warnings.warn('Please specify an x_range with GoogleOMeterChart, '
1054
'otherwise one arrow will always be at the max.')
898
1056
def type_to_url(self):
899
1057
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__':
1060
class QRChart(Chart):
1062
def __init__(self, *args, **kwargs):
1063
Chart.__init__(self, *args, **kwargs)
1064
self.encoding = None
1065
self.ec_level = None
1068
def type_to_url(self):
1071
def data_to_url(self, data_class=None):
1073
raise NoDataGivenException()
1074
return 'chl=%s' % urllib.quote(self.data[0])
1076
def get_url_bits(self, data_class=None):
1077
url_bits = Chart.get_url_bits(self, data_class=data_class)
1079
url_bits.append('choe=%s' % self.encoding)
1081
url_bits.append('chld=%s%%7c%s' % (self.ec_level, self.margin))
1084
def set_encoding(self, encoding):
1085
self.encoding = encoding
1087
def set_ec(self, level, margin):
1088
self.ec_level = level
1089
self.margin = margin
1092
class ChartGrammar(object):
1098
def parse(self, grammar):
1099
self.grammar = grammar
1100
self.chart = self.create_chart_instance()
1102
for attr in self.grammar:
1103
if attr in ('w', 'h', 'type', 'auto_scale', 'x_range', 'y_range'):
1104
continue # These are already parsed in create_chart_instance
1105
attr_func = 'parse_' + attr
1106
if not hasattr(self, attr_func):
1107
warnings.warn('No parser for grammar attribute "%s"' % (attr))
1109
getattr(self, attr_func)(grammar[attr])
1113
def parse_data(self, data):
1114
self.chart.data = data
1117
def get_possible_chart_types():
1118
possible_charts = []
1119
for cls_name in globals().keys():
1120
if not cls_name.endswith('Chart'):
1122
cls = globals()[cls_name]
1123
# Check if it is an abstract class
1125
a = cls(1, 1, auto_scale=False)
1127
except AbstractClassException:
1130
possible_charts.append(cls_name[:-5])
1131
return possible_charts
1133
def create_chart_instance(self, grammar=None):
1135
grammar = self.grammar
1136
assert(isinstance(grammar, dict)) # grammar must be a dict
1137
assert('w' in grammar) # width is required
1138
assert('h' in grammar) # height is required
1139
assert('type' in grammar) # type is required
1140
chart_type = grammar['type']
1143
auto_scale = grammar.get('auto_scale', None)
1144
x_range = grammar.get('x_range', None)
1145
y_range = grammar.get('y_range', None)
1146
types = ChartGrammar.get_possible_chart_types()
1147
if chart_type not in types:
1148
raise UnknownChartType('%s is an unknown chart type. Possible '
1149
'chart types are %s' % (chart_type, ','.join(types)))
1150
return globals()[chart_type + 'Chart'](w, h, auto_scale=auto_scale,
1151
x_range=x_range, y_range=y_range)