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