76
93
class Data(object):
78
95
def __init__(self, data):
79
assert(type(self) != Data) # This is an abstract class
96
if type(self) == Data:
97
raise AbstractClassException('This is an abstract class')
83
101
def float_scale_value(cls, value, range):
84
102
lower, upper = range
85
max_value = cls.max_value()
86
scaled = (value-lower) * (float(max_value)/(upper-lower))
103
assert(upper > lower)
104
scaled = (value - lower) * (cls.max_value / (upper - lower))
90
108
def clip_value(cls, value):
91
clipped = max(0, min(value, cls.max_value()))
109
return max(0, min(value, cls.max_value))
95
112
def int_scale_value(cls, value, range):
96
scaled = int(round(cls.float_scale_value(value, range)))
113
return int(round(cls.float_scale_value(value, range)))
100
116
def scale_value(cls, value, range):
101
117
scaled = cls.int_scale_value(value, range)
102
118
clipped = cls.clip_value(scaled)
119
Data.check_clip(scaled, clipped)
123
def check_clip(scaled, clipped):
124
if clipped != scaled:
125
warnings.warn('One or more of of your data points has been '
126
'clipped because it is out of range.')
105
129
class SimpleData(Data):
106
132
enc_map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
108
134
def __repr__(self):
109
max_value = self.max_value()
110
135
encoded_data = []
111
136
for data in self.data:
113
138
for value in data:
114
139
if value is None:
115
140
sub_data.append('_')
116
elif value >= 0 and value <= max_value:
141
elif value >= 0 and value <= self.max_value:
117
142
sub_data.append(SimpleData.enc_map[value])
119
144
raise DataOutOfRangeException('cannot encode value: %d'
121
146
encoded_data.append(''.join(sub_data))
122
147
return 'chd=s:' + ','.join(encoded_data)
128
150
class TextData(Data):
130
154
def __repr__(self):
131
max_value = self.max_value()
132
155
encoded_data = []
133
156
for data in self.data:
135
158
for value in data:
136
159
if value is None:
137
160
sub_data.append(-1)
138
elif value >= 0 and value <= max_value:
161
elif value >= 0 and value <= self.max_value:
139
162
sub_data.append("%.1f" % float(value))
141
164
raise DataOutOfRangeException()
142
165
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))
166
return 'chd=t:' + '%7c'.join(encoded_data)
161
169
def scale_value(cls, value, range):
162
170
# use float values instead of integers because we don't need an encode
164
scaled = cls.float_scale_value(value,range)
172
scaled = cls.float_scale_value(value, range)
165
173
clipped = cls.clip_value(scaled)
174
Data.check_clip(scaled, clipped)
168
178
class ExtendedData(Data):
170
182
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-.'
172
184
def __repr__(self):
173
max_value = self.max_value()
174
185
encoded_data = []
175
186
enc_size = len(ExtendedData.enc_map)
176
187
for data in self.data:
284
292
LINEAR_STRIPES = 'ls'
286
294
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
295
auto_scale=True, x_range=None, y_range=None,
296
colours_within_series=None):
297
if type(self) == Chart:
298
raise AbstractClassException('This is an abstract class')
289
299
assert(isinstance(width, int))
290
300
assert(isinstance(height, int))
291
301
self.width = width
292
302
self.height = height
294
304
self.set_title(title)
305
self.set_title_style(None, None)
295
306
self.set_legend(legend)
307
self.set_legend_position(None)
296
308
self.set_colours(colours)
309
self.set_colours_within_series(colours_within_series)
298
311
# 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
312
self.auto_scale = auto_scale # Whether to automatically scale data
313
self.x_range = x_range # (min, max) x-axis range for scaling
314
self.y_range = y_range # (min, max) y-axis range for scaling
302
315
self.scaled_data_class = None
303
316
self.scaled_x_range = None
304
317
self.scaled_y_range = None
610
665
url_bits.append('chxt=%s' % ','.join(available_axis))
612
url_bits.append('chxl=%s' % '|'.join(label_axis))
667
url_bits.append('chxl=%s' % '%7c'.join(label_axis))
614
url_bits.append('chxr=%s' % '|'.join(range_axis))
669
url_bits.append('chxr=%s' % '%7c'.join(range_axis))
616
url_bits.append('chxp=%s' % '|'.join(positions))
671
url_bits.append('chxp=%s' % '%7c'.join(positions))
618
url_bits.append('chxs=%s' % '|'.join(styles))
673
url_bits.append('chxs=%s' % '%7c'.join(styles))
619
674
return '&'.join(url_bits)
621
676
# Markers, Ranges and Fill area (chm)
622
677
# -------------------------------------------------------------------------
624
def markers_to_url(self):
625
return 'chm=%s' % '|'.join([','.join(a) for a in self.markers])
679
def markers_to_url(self):
680
return 'chm=%s' % '%7c'.join([','.join(a) for a in self.markers])
627
682
def add_marker(self, index, point, marker_type, colour, size, priority=0):
628
683
self.markers.append((marker_type, colour, str(index), str(point), \
629
684
str(size), str(priority)))
631
686
def add_horizontal_range(self, colour, start, stop):
632
self.markers.append(('r', colour, '1', str(start), str(stop)))
687
self.markers.append(('r', colour, '0', str(start), str(stop)))
689
def add_data_line(self, colour, data_set, size, priority=0):
690
self.markers.append(('D', colour, str(data_set), '0', str(size), \
693
def add_marker_text(self, string, colour, data_set, data_point, size, \
695
self.markers.append((str(string), colour, str(data_set), \
696
str(data_point), str(size), str(priority)))
634
698
def add_vertical_range(self, colour, start, stop):
635
self.markers.append(('R', colour, '1', str(start), str(stop)))
699
self.markers.append(('R', colour, '0', str(start), str(stop)))
637
701
def add_fill_range(self, colour, index_start, index_end):
638
702
self.markers.append(('b', colour, str(index_start), str(index_end), \
895
972
class GoogleOMeterChart(PieChart):
896
973
"""Inheriting from PieChart because of similar labeling"""
975
def __init__(self, *args, **kwargs):
976
PieChart.__init__(self, *args, **kwargs)
977
if self.auto_scale and not self.x_range:
978
warnings.warn('Please specify an x_range with GoogleOMeterChart, '
979
'otherwise one arrow will always be at the max.')
898
981
def type_to_url(self):
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__':
985
class QRChart(Chart):
987
def __init__(self, *args, **kwargs):
988
Chart.__init__(self, *args, **kwargs)
993
def type_to_url(self):
996
def data_to_url(self, data_class=None):
998
raise NoDataGivenException()
999
return 'chl=%s' % urllib.quote(self.data[0])
1001
def get_url_bits(self, data_class=None):
1002
url_bits = Chart.get_url_bits(self, data_class=data_class)
1004
url_bits.append('choe=%s' % self.encoding)
1006
url_bits.append('chld=%s%%7c%s' % (self.ec_level, self.margin))
1009
def set_encoding(self, encoding):
1010
self.encoding = encoding
1012
def set_ec(self, level, margin):
1013
self.ec_level = level
1014
self.margin = margin
1017
class ChartGrammar(object):
1023
def parse(self, grammar):
1024
self.grammar = grammar
1025
self.chart = self.create_chart_instance()
1027
for attr in self.grammar:
1028
if attr in ('w', 'h', 'type', 'auto_scale', 'x_range', 'y_range'):
1029
continue # These are already parsed in create_chart_instance
1030
attr_func = 'parse_' + attr
1031
if not hasattr(self, attr_func):
1032
warnings.warn('No parser for grammar attribute "%s"' % (attr))
1034
getattr(self, attr_func)(grammar[attr])
1038
def parse_data(self, data):
1039
self.chart.data = data
1042
def get_possible_chart_types():
1043
possible_charts = []
1044
for cls_name in globals().keys():
1045
if not cls_name.endswith('Chart'):
1047
cls = globals()[cls_name]
1048
# Check if it is an abstract class
1050
a = cls(1, 1, auto_scale=False)
1052
except AbstractClassException:
1055
possible_charts.append(cls_name[:-5])
1056
return possible_charts
1058
def create_chart_instance(self, grammar=None):
1060
grammar = self.grammar
1061
assert(isinstance(grammar, dict)) # grammar must be a dict
1062
assert('w' in grammar) # width is required
1063
assert('h' in grammar) # height is required
1064
assert('type' in grammar) # type is required
1065
chart_type = grammar['type']
1068
auto_scale = grammar.get('auto_scale', None)
1069
x_range = grammar.get('x_range', None)
1070
y_range = grammar.get('y_range', None)
1071
types = ChartGrammar.get_possible_chart_types()
1072
if chart_type not in types:
1073
raise UnknownChartType('%s is an unknown chart type. Possible '
1074
'chart types are %s' % (chart_type, ','.join(types)))
1075
return globals()[chart_type + 'Chart'](w, h, auto_scale=auto_scale,
1076
x_range=x_range, y_range=y_range)