93
76
class Data(object):
95
78
def __init__(self, data):
96
if type(self) == Data:
97
raise AbstractClassException('This is an abstract class')
79
assert(type(self) != Data) # This is an abstract class
101
def float_scale_value(cls, value, range):
103
assert(upper > lower)
104
scaled = (value - lower) * (cls.max_value / (upper - lower))
108
def clip_value(cls, value):
109
return max(0, min(value, cls.max_value))
112
def int_scale_value(cls, value, range):
113
return int(round(cls.float_scale_value(value, range)))
116
def scale_value(cls, value, range):
117
scaled = cls.int_scale_value(value, range)
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.')
129
83
class SimpleData(Data):
132
84
enc_map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
134
86
def __repr__(self):
87
max_value = self.max_value()
136
89
for data in self.data:
138
91
for value in data:
140
93
sub_data.append('_')
141
elif value >= 0 and value <= self.max_value:
94
elif value >= 0 and value <= max_value:
142
95
sub_data.append(SimpleData.enc_map[value])
144
97
raise DataOutOfRangeException('cannot encode value: %d'
146
99
encoded_data.append(''.join(sub_data))
147
100
return 'chd=s:' + ','.join(encoded_data)
107
def scale_value(cls, value, range):
109
max_value = cls.max_value()
110
scaled = int(round((float(value) - lower) * max_value / upper))
111
clipped = max(0, min(scaled, max_value))
150
114
class TextData(Data):
154
116
def __repr__(self):
117
max_value = self.max_value()
155
118
encoded_data = []
156
119
for data in self.data:
158
121
for value in data:
159
122
if value is None:
160
123
sub_data.append(-1)
161
elif value >= 0 and value <= self.max_value:
124
elif value >= 0 and value <= max_value:
162
125
sub_data.append("%.1f" % float(value))
164
127
raise DataOutOfRangeException()
165
128
encoded_data.append(','.join(sub_data))
166
return 'chd=t:' + '%7c'.join(encoded_data)
129
return 'chd=t:' + '|'.join(encoded_data)
169
136
def scale_value(cls, value, range):
170
# use float values instead of integers because we don't need an encode
172
scaled = cls.float_scale_value(value, range)
173
clipped = cls.clip_value(scaled)
174
Data.check_clip(scaled, clipped)
138
max_value = cls.max_value()
139
scaled = (float(value) - lower) * max_value / upper
140
clipped = max(0, min(scaled, max_value))
178
143
class ExtendedData(Data):
182
145
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-.'
184
147
def __repr__(self):
148
max_value = self.max_value()
185
149
encoded_data = []
186
150
enc_size = len(ExtendedData.enc_map)
187
151
for data in self.data:
285
260
BASE_URL = 'http://chart.apis.google.com/chart?'
286
261
BACKGROUND = 'bg'
289
VALID_SOLID_FILL_TYPES = (BACKGROUND, CHART, ALPHA)
291
264
LINEAR_GRADIENT = 'lg'
292
265
LINEAR_STRIPES = 'ls'
294
267
def __init__(self, width, height, title=None, legend=None, colours=None,
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')
268
auto_scale=True, x_range=None, y_range=None):
269
assert(type(self) != Chart) # This is an abstract class
299
270
assert(isinstance(width, int))
300
271
assert(isinstance(height, int))
301
272
self.width = width
302
273
self.height = height
304
275
self.set_title(title)
305
self.set_title_style(None, None)
306
276
self.set_legend(legend)
307
self.set_legend_position(None)
308
277
self.set_colours(colours)
309
self.set_colours_within_series(colours_within_series)
311
279
# Data 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
280
self.auto_scale = auto_scale # Whether to automatically scale data
281
self.x_range = x_range # (min, max) x-axis range for scaling
282
self.y_range = y_range # (min, max) y-axis range for scaling
315
283
self.scaled_data_class = None
316
284
self.scaled_x_range = None
317
285
self.scaled_y_range = None
319
287
self.fill_types = {
320
288
Chart.BACKGROUND: None,
321
289
Chart.CHART: None,
324
291
self.fill_area = {
325
292
Chart.BACKGROUND: None,
326
293
Chart.CHART: None,
330
296
self.markers = []
331
self.line_styles = {}
335
299
# -------------------------------------------------------------------------
337
def get_url(self, data_class=None):
338
url_bits = self.get_url_bits(data_class=data_class)
302
url_bits = self.get_url_bits()
339
303
return self.BASE_URL + '&'.join(url_bits)
341
def get_url_bits(self, data_class=None):
305
def get_url_bits(self):
343
307
# required arguments
344
308
url_bits.append(self.type_to_url())
345
309
url_bits.append('chs=%ix%i' % (self.width, self.height))
346
url_bits.append(self.data_to_url(data_class=data_class))
310
url_bits.append(self.data_to_url())
347
311
# optional arguments
349
313
url_bits.append('chtt=%s' % self.title)
350
if self.title_colour and self.title_font_size:
351
url_bits.append('chts=%s,%s' % (self.title_colour, \
352
self.title_font_size))
354
url_bits.append('chdl=%s' % '%7c'.join(self.legend))
355
if self.legend_position:
356
url_bits.append('chdlp=%s' % (self.legend_position))
315
url_bits.append('chdl=%s' % '|'.join(self.legend))
358
url_bits.append('chco=%s' % ','.join(self.colours))
359
if self.colours_within_series:
360
url_bits.append('chco=%s' % '%7c'.join(self.colours_within_series))
317
url_bits.append('chco=%s' % ','.join(self.colours))
361
318
ret = self.fill_to_url()
363
320
url_bits.append(ret)
364
321
ret = self.axis_to_url()
368
url_bits.append(self.markers_to_url())
371
for index in xrange(max(self.line_styles) + 1):
372
if index in self.line_styles:
373
values = self.line_styles[index]
376
style.append(','.join(values))
377
url_bits.append('chls=%s' % '%7c'.join(style))
379
url_bits.append('chg=%s' % self.grid)
325
url_bits.append(self.markers_to_url())
469
393
def fill_linear_gradient(self, area, angle, *args):
470
assert(area in Chart.VALID_SOLID_FILL_TYPES)
394
assert(area in (Chart.BACKGROUND, Chart.CHART))
471
395
args = self._check_fill_linear(angle, *args)
472
396
self.fill_types[area] = Chart.LINEAR_GRADIENT
473
397
self.fill_area[area] = ','.join([str(angle)] + args)
475
399
def fill_linear_stripes(self, area, angle, *args):
476
assert(area in Chart.VALID_SOLID_FILL_TYPES)
400
assert(area in (Chart.BACKGROUND, Chart.CHART))
477
401
args = self._check_fill_linear(angle, *args)
478
402
self.fill_types[area] = Chart.LINEAR_STRIPES
479
403
self.fill_area[area] = ','.join([str(angle)] + args)
481
405
def fill_to_url(self):
483
for area in (Chart.BACKGROUND, Chart.CHART, Chart.ALPHA):
407
for area in (Chart.BACKGROUND, Chart.CHART):
484
408
if self.fill_types[area]:
485
409
areas.append('%s,%s,%s' % (area, self.fill_types[area], \
486
410
self.fill_area[area]))
488
return 'chf=' + '%7c'.join(areas)
412
return 'chf=' + '|'.join(areas)
491
415
# -------------------------------------------------------------------------
665
574
url_bits.append('chxt=%s' % ','.join(available_axis))
667
url_bits.append('chxl=%s' % '%7c'.join(label_axis))
576
url_bits.append('chxl=%s' % '|'.join(label_axis))
669
url_bits.append('chxr=%s' % '%7c'.join(range_axis))
578
url_bits.append('chxr=%s' % '|'.join(range_axis))
671
url_bits.append('chxp=%s' % '%7c'.join(positions))
580
url_bits.append('chxp=%s' % '|'.join(positions))
673
url_bits.append('chxs=%s' % '%7c'.join(styles))
582
url_bits.append('chxs=%s' % '|'.join(styles))
674
583
return '&'.join(url_bits)
676
585
# Markers, Ranges and Fill area (chm)
677
586
# -------------------------------------------------------------------------
679
def markers_to_url(self):
680
return 'chm=%s' % '%7c'.join([','.join(a) for a in self.markers])
588
def markers_to_url(self):
589
return 'chm=%s' % '|'.join([','.join(a) for a in self.markers])
682
def add_marker(self, index, point, marker_type, colour, size, priority=0):
591
def add_marker(self, index, point, marker_type, colour, size):
683
592
self.markers.append((marker_type, colour, str(index), str(point), \
684
str(size), str(priority)))
686
595
def add_horizontal_range(self, colour, start, 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)))
596
self.markers.append(('r', colour, '1', str(start), str(stop)))
698
598
def add_vertical_range(self, colour, start, stop):
699
self.markers.append(('R', colour, '0', str(start), str(stop)))
599
self.markers.append(('R', colour, '1', str(start), str(stop)))
701
601
def add_fill_range(self, colour, index_start, index_end):
702
602
self.markers.append(('b', colour, str(index_start), str(index_end), \
717
636
value.append(str(blank_segment))
718
637
self.line_styles[index] = value
721
# -------------------------------------------------------------------------
723
639
def set_grid(self, x_step, y_step, line_segment=1, \
724
640
blank_segment=0):
725
641
self.grid = '%s,%s,%s,%s' % (x_step, y_step, line_segment, \
729
class ScatterChart(Chart):
731
def type_to_url(self):
734
def annotated_data(self):
735
yield ('x', self.data[0])
736
yield ('y', self.data[1])
737
if len(self.data) > 2:
738
# The optional third dataset is relative sizing for point
740
yield ('marker-size', self.data[2])
743
class LineChart(Chart):
745
def __init__(self, *args, **kwargs):
746
if type(self) == LineChart:
747
raise AbstractClassException('This is an abstract class')
748
Chart.__init__(self, *args, **kwargs)
644
def get_url_bits(self):
645
url_bits = Chart.get_url_bits(self)
648
# for index, values in self.line_style.items():
649
for index in xrange(max(self.line_styles) + 1):
650
if index in self.line_styles:
651
values = self.line_styles[index]
654
style.append(','.join(values))
655
url_bits.append('chls=%s' % '|'.join(style))
657
url_bits.append('chg=%s' % self.grid)
751
661
class SimpleLineChart(LineChart):
885
784
class PieChart(Chart):
887
786
def __init__(self, *args, **kwargs):
888
if type(self) == PieChart:
889
raise AbstractClassException('This is an abstract class')
787
assert(type(self) != PieChart) # This is an abstract class
890
788
Chart.__init__(self, *args, **kwargs)
891
789
self.pie_labels = []
893
warnings.warn('y_range is not used with %s.' % \
894
(self.__class__.__name__))
896
791
def set_pie_labels(self, labels):
897
792
self.pie_labels = [urllib.quote(a) for a in labels]
899
def get_url_bits(self, data_class=None):
900
url_bits = Chart.get_url_bits(self, data_class=data_class)
794
def get_url_bits(self):
795
url_bits = Chart.get_url_bits(self)
901
796
if self.pie_labels:
902
url_bits.append('chl=%s' % '%7c'.join(self.pie_labels))
797
url_bits.append('chl=%s' % '|'.join(self.pie_labels))
905
800
def annotated_data(self):
906
801
# Datasets are all y-axis data. However, there should only be
907
802
# one dataset for pie charts.
908
803
for dataset in self.data:
911
def scaled_data(self, data_class, x_range=None, y_range=None):
913
x_range = [0, sum(self.data[0])]
914
return Chart.scaled_data(self, data_class, x_range, self.y_range)
917
807
class PieChart2D(PieChart):
936
826
yield ('y', dataset)
939
class RadarChart(Chart):
941
def type_to_url(self):
945
class SplineRadarChart(RadarChart):
947
def type_to_url(self):
951
class MapChart(Chart):
953
def __init__(self, *args, **kwargs):
954
Chart.__init__(self, *args, **kwargs)
955
self.geo_area = 'world'
958
def type_to_url(self):
961
def set_codes(self, codes):
964
def get_url_bits(self, data_class=None):
965
url_bits = Chart.get_url_bits(self, data_class=data_class)
966
url_bits.append('chtm=%s' % self.geo_area)
968
url_bits.append('chld=%s' % ''.join(self.codes))
972
class GoogleOMeterChart(PieChart):
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.')
981
def type_to_url(self):
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)
830
chart = GroupedVerticalBarChart(320, 200)
831
chart = PieChart2D(320, 200)
832
chart = ScatterChart(320, 200)
833
chart = SimpleLineChart(320, 200)
834
sine_data = [math.sin(float(a) / 10) * 2000 + 2000 for a in xrange(100)]
835
random_data = [a * random.random() * 30 for a in xrange(40)]
836
random_data2 = [random.random() * 4000 for a in xrange(10)]
837
# chart.set_bar_width(50)
838
# chart.set_bar_spacing(0)
839
chart.add_data(sine_data)
840
chart.add_data(random_data)
841
chart.add_data(random_data2)
842
# chart.set_line_style(1, thickness=2)
843
# chart.set_line_style(2, line_segment=10, blank_segment=5)
844
# chart.set_title('heloooo')
845
# chart.set_legend(('sine wave', 'random * x'))
846
# chart.set_colours(('ee2000', 'DDDDAA', 'fF03f2'))
847
# chart.fill_solid(Chart.BACKGROUND, '123456')
848
# chart.fill_linear_gradient(Chart.CHART, 20, '004070', 1, '300040', 0,
850
# chart.fill_linear_stripes(Chart.CHART, 20, '204070', .2, '300040', .2,
852
axis_left_index = chart.set_axis_range(Axis.LEFT, 0, 10)
853
axis_left_index = chart.set_axis_range(Axis.LEFT, 0, 10)
854
axis_left_index = chart.set_axis_range(Axis.LEFT, 0, 10)
855
axis_right_index = chart.set_axis_range(Axis.RIGHT, 5, 30)
856
axis_bottom_index = chart.set_axis_labels(Axis.BOTTOM, [1, 25, 95])
857
chart.set_axis_positions(axis_bottom_index, [1, 25, 95])
858
chart.set_axis_style(axis_bottom_index, '003050', 15)
860
# chart.set_pie_labels(('apples', 'oranges', 'bananas'))
862
# chart.set_grid(10, 10)
864
# for a in xrange(0, 100, 10):
865
# chart.add_marker(1, a, 'a', 'AACA20', 10)
867
chart.add_horizontal_range('00A020', .2, .5)
868
chart.add_vertical_range('00c030', .2, .4)
870
chart.add_fill_simple('303030A0')
872
chart.download('test.png')
874
url = chart.get_url()
877
data = urllib.urlopen(chart.get_url()).read()
878
open('meh.png', 'wb').write(data)
879
os.system('start meh.png')
882
if __name__ == '__main__':