92
101
def float_scale_value(cls, value, range):
93
102
lower, upper = range
94
103
assert(upper > lower)
95
max_value = cls.max_value()
96
scaled = (value-lower) * (float(max_value) / (upper - lower))
104
scaled = (value - lower) * (cls.max_value / (upper - lower))
100
108
def clip_value(cls, value):
101
return max(0, min(value, cls.max_value()))
109
return max(0, min(value, cls.max_value))
104
112
def int_scale_value(cls, value, range):
108
116
def scale_value(cls, value, range):
109
117
scaled = cls.int_scale_value(value, range)
110
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.')
114
129
class SimpleData(Data):
116
132
enc_map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
118
134
def __repr__(self):
119
max_value = self.max_value()
120
135
encoded_data = []
121
136
for data in self.data:
123
138
for value in data:
124
139
if value is None:
125
140
sub_data.append('_')
126
elif value >= 0 and value <= max_value:
141
elif value >= 0 and value <= self.max_value:
127
142
sub_data.append(SimpleData.enc_map[value])
129
144
raise DataOutOfRangeException('cannot encode value: %d'
131
146
encoded_data.append(''.join(sub_data))
132
147
return 'chd=s:' + ','.join(encoded_data)
139
150
class TextData(Data):
141
154
def __repr__(self):
142
max_value = self.max_value()
143
155
encoded_data = []
144
156
for data in self.data:
146
158
for value in data:
147
159
if value is None:
148
160
sub_data.append(-1)
149
elif value >= 0 and value <= max_value:
161
elif value >= 0 and value <= self.max_value:
150
162
sub_data.append("%.1f" % float(value))
152
164
raise DataOutOfRangeException()
153
165
encoded_data.append(','.join(sub_data))
154
return 'chd=t:' + '|'.join(encoded_data)
166
return 'chd=t:' + '%7c'.join(encoded_data)
161
169
def scale_value(cls, value, range):
296
302
self.height = height
298
304
self.set_title(title)
305
self.set_title_style(None, None)
299
306
self.set_legend(legend)
307
self.set_legend_position(None)
300
308
self.set_colours(colours)
309
self.set_colours_within_series(colours_within_series)
302
311
# Data for scaling.
303
self.auto_scale = auto_scale # Whether to automatically scale data
304
self.x_range = x_range # (min, max) x-axis range for scaling
305
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
306
315
self.scaled_data_class = None
307
316
self.scaled_x_range = None
308
317
self.scaled_y_range = None
338
347
# optional arguments
340
349
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))
342
url_bits.append('chdl=%s' % '|'.join(self.legend))
354
url_bits.append('chdl=%s' % '%7c'.join(self.legend))
355
if self.legend_position:
356
url_bits.append('chdlp=%s' % (self.legend_position))
344
url_bits.append('chco=%s' % ','.join(self.colours))
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))
345
361
ret = self.fill_to_url()
347
363
url_bits.append(ret)
348
364
ret = self.axis_to_url()
352
url_bits.append(self.markers_to_url())
368
url_bits.append(self.markers_to_url())
353
369
if self.line_styles:
355
371
for index in xrange(max(self.line_styles) + 1):
517
562
if x_range is None:
518
563
x_range = self.data_x_range()
519
564
if x_range and x_range[0] > 0:
520
x_range = (0, x_range[1])
565
x_range = (x_range[0], x_range[1])
521
566
self.scaled_x_range = x_range
523
568
# Determine the y-axis range for scaling.
524
569
if y_range is None:
525
570
y_range = self.data_y_range()
526
571
if y_range and y_range[0] > 0:
527
y_range = (0, y_range[1])
572
y_range = (y_range[0], y_range[1])
528
573
self.scaled_y_range = y_range
615
665
url_bits.append('chxt=%s' % ','.join(available_axis))
617
url_bits.append('chxl=%s' % '|'.join(label_axis))
667
url_bits.append('chxl=%s' % '%7c'.join(label_axis))
619
url_bits.append('chxr=%s' % '|'.join(range_axis))
669
url_bits.append('chxr=%s' % '%7c'.join(range_axis))
621
url_bits.append('chxp=%s' % '|'.join(positions))
671
url_bits.append('chxp=%s' % '%7c'.join(positions))
623
url_bits.append('chxs=%s' % '|'.join(styles))
673
url_bits.append('chxs=%s' % '%7c'.join(styles))
624
674
return '&'.join(url_bits)
626
676
# Markers, Ranges and Fill area (chm)
627
677
# -------------------------------------------------------------------------
629
def markers_to_url(self):
630
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])
632
682
def add_marker(self, index, point, marker_type, colour, size, priority=0):
633
683
self.markers.append((marker_type, colour, str(index), str(point), \
634
684
str(size), str(priority)))
636
686
def add_horizontal_range(self, colour, start, stop):
637
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)))
639
698
def add_vertical_range(self, colour, start, stop):
640
self.markers.append(('R', colour, '1', str(start), str(stop)))
699
self.markers.append(('R', colour, '0', str(start), str(stop)))
642
701
def add_fill_range(self, colour, index_start, index_end):
643
702
self.markers.append(('b', colour, str(index_start), str(index_end), \
837
899
def get_url_bits(self, data_class=None):
838
900
url_bits = Chart.get_url_bits(self, data_class=data_class)
839
901
if self.pie_labels:
840
url_bits.append('chl=%s' % '|'.join(self.pie_labels))
902
url_bits.append('chl=%s' % '%7c'.join(self.pie_labels))
843
905
def annotated_data(self):
844
906
# Datasets are all y-axis data. However, there should only be
845
907
# one dataset for pie charts.
846
908
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)
850
917
class PieChart2D(PieChart):
905
972
class GoogleOMeterChart(PieChart):
906
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.')
908
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
912
1017
class ChartGrammar(object):
914
def __init__(self, grammar):
1023
def parse(self, grammar):
915
1024
self.grammar = grammar
916
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
919
1042
def get_possible_chart_types():
920
1043
possible_charts = []
921
for cls_name in globals():
1044
for cls_name in globals().keys():
922
1045
if not cls_name.endswith('Chart'):
924
1047
cls = globals()[cls_name]
925
1048
# Check if it is an abstract class
1050
a = cls(1, 1, auto_scale=False)
928
1052
except AbstractClassException:
930
1054
# Strip off "Class"
931
1055
possible_charts.append(cls_name[:-5])
932
1056
return possible_charts
934
def create_chart_instance(self):
1058
def create_chart_instance(self, grammar=None):
1060
grammar = self.grammar
1061
assert(isinstance(grammar, dict)) # grammar must be a dict
935
1062
assert('w' in grammar) # width is required
936
1063
assert('h' in grammar) # height is required
937
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)
938
1071
types = ChartGrammar.get_possible_chart_types()
939
if grammar['type'] not in types:
1072
if chart_type not in types:
940
1073
raise UnknownChartType('%s is an unknown chart type. Possible '
941
'chart types are %s' % (grammar['type'], ','.join(types)))
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)
943
1078
def download(self):