95
76
class Data(object):
97
78
def __init__(self, data):
98
if type(self) == Data:
99
raise AbstractClassException('This is an abstract class')
79
assert(type(self) != Data) # This is an abstract class
103
83
def float_scale_value(cls, value, range):
104
84
lower, upper = range
105
assert(upper > lower)
106
scaled = (value - lower) * (cls.max_value / (upper - lower))
85
max_value = cls.max_value()
86
scaled = (value-lower) * (float(max_value)/(upper-lower))
110
90
def clip_value(cls, value):
111
return max(0, min(value, cls.max_value))
91
clipped = max(0, min(value, cls.max_value()))
114
95
def int_scale_value(cls, value, range):
115
return int(round(cls.float_scale_value(value, range)))
96
scaled = int(round(cls.float_scale_value(value, range)))
118
100
def scale_value(cls, value, range):
119
101
scaled = cls.int_scale_value(value, range)
120
102
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.')
131
105
class SimpleData(Data):
134
106
enc_map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
136
108
def __repr__(self):
109
max_value = self.max_value()
137
110
encoded_data = []
138
111
for data in self.data:
140
113
for value in data:
141
114
if value is None:
142
115
sub_data.append('_')
143
elif value >= 0 and value <= self.max_value:
116
elif value >= 0 and value <= max_value:
144
117
sub_data.append(SimpleData.enc_map[value])
146
119
raise DataOutOfRangeException('cannot encode value: %d'
148
121
encoded_data.append(''.join(sub_data))
149
122
return 'chd=s:' + ','.join(encoded_data)
152
128
class TextData(Data):
156
130
def __repr__(self):
131
max_value = self.max_value()
157
132
encoded_data = []
158
133
for data in self.data:
160
135
for value in data:
161
136
if value is None:
162
137
sub_data.append(-1)
163
elif value >= 0 and value <= self.max_value:
138
elif value >= 0 and value <= max_value:
164
139
sub_data.append("%.1f" % float(value))
166
141
raise DataOutOfRangeException()
167
142
encoded_data.append(','.join(sub_data))
168
return 'chd=t:' + '%7c'.join(encoded_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))
171
161
def scale_value(cls, value, range):
172
162
# use float values instead of integers because we don't need an encode
174
scaled = cls.float_scale_value(value, range)
164
scaled = cls.float_scale_value(value,range)
175
165
clipped = cls.clip_value(scaled)
176
Data.check_clip(scaled, clipped)
180
168
class ExtendedData(Data):
184
170
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-.'
186
172
def __repr__(self):
173
max_value = self.max_value()
187
174
encoded_data = []
188
175
enc_size = len(ExtendedData.enc_map)
189
176
for data in self.data:
287
277
BASE_URL = 'http://chart.apis.google.com/chart?'
288
278
BACKGROUND = 'bg'
291
VALID_SOLID_FILL_TYPES = (BACKGROUND, CHART, ALPHA)
293
281
LINEAR_GRADIENT = 'lg'
294
282
LINEAR_STRIPES = 'ls'
296
284
def __init__(self, width, height, title=None, legend=None, colours=None,
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')
285
auto_scale=True, x_range=None, y_range=None):
286
assert(type(self) != Chart) # This is an abstract class
301
287
assert(isinstance(width, int))
302
288
assert(isinstance(height, int))
303
289
self.width = width
304
290
self.height = height
306
292
self.set_title(title)
307
self.set_title_style(None, None)
308
293
self.set_legend(legend)
309
self.set_legend_position(None)
310
294
self.set_colours(colours)
311
self.set_colours_within_series(colours_within_series)
313
296
# Data 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
297
self.auto_scale = auto_scale # Whether to automatically scale data
298
self.x_range = x_range # (min, max) x-axis range for scaling
299
self.y_range = y_range # (min, max) y-axis range for scaling
317
300
self.scaled_data_class = None
318
301
self.scaled_x_range = None
319
302
self.scaled_y_range = None
471
423
def fill_linear_gradient(self, area, angle, *args):
472
assert(area in Chart.VALID_SOLID_FILL_TYPES)
424
assert(area in (Chart.BACKGROUND, Chart.CHART))
473
425
args = self._check_fill_linear(angle, *args)
474
426
self.fill_types[area] = Chart.LINEAR_GRADIENT
475
427
self.fill_area[area] = ','.join([str(angle)] + args)
477
429
def fill_linear_stripes(self, area, angle, *args):
478
assert(area in Chart.VALID_SOLID_FILL_TYPES)
430
assert(area in (Chart.BACKGROUND, Chart.CHART))
479
431
args = self._check_fill_linear(angle, *args)
480
432
self.fill_types[area] = Chart.LINEAR_STRIPES
481
433
self.fill_area[area] = ','.join([str(angle)] + args)
483
435
def fill_to_url(self):
485
for area in (Chart.BACKGROUND, Chart.CHART, Chart.ALPHA):
437
for area in (Chart.BACKGROUND, Chart.CHART):
486
438
if self.fill_types[area]:
487
439
areas.append('%s,%s,%s' % (area, self.fill_types[area], \
488
440
self.fill_area[area]))
490
return 'chf=' + '%7c'.join(areas)
442
return 'chf=' + '|'.join(areas)
493
445
# -------------------------------------------------------------------------
667
602
url_bits.append('chxt=%s' % ','.join(available_axis))
669
url_bits.append('chxl=%s' % '%7c'.join(label_axis))
604
url_bits.append('chxl=%s' % '|'.join(label_axis))
671
url_bits.append('chxr=%s' % '%7c'.join(range_axis))
606
url_bits.append('chxr=%s' % '|'.join(range_axis))
673
url_bits.append('chxp=%s' % '%7c'.join(positions))
608
url_bits.append('chxp=%s' % '|'.join(positions))
675
url_bits.append('chxs=%s' % '%7c'.join(styles))
610
url_bits.append('chxs=%s' % '|'.join(styles))
676
611
return '&'.join(url_bits)
678
613
# Markers, Ranges and Fill area (chm)
679
614
# -------------------------------------------------------------------------
681
def markers_to_url(self):
682
return 'chm=%s' % '%7c'.join([','.join(a) for a in self.markers])
616
def markers_to_url(self):
617
return 'chm=%s' % '|'.join([','.join(a) for a in self.markers])
684
def add_marker(self, index, point, marker_type, colour, size, priority=0):
619
def add_marker(self, index, point, marker_type, colour, size):
685
620
self.markers.append((marker_type, colour, str(index), str(point), \
686
str(size), str(priority)))
688
623
def add_horizontal_range(self, colour, start, 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)))
624
self.markers.append(('r', colour, '1', str(start), str(stop)))
700
626
def add_vertical_range(self, colour, start, stop):
701
self.markers.append(('R', colour, '0', str(start), str(stop)))
627
self.markers.append(('R', colour, '1', str(start), str(stop)))
703
629
def add_fill_range(self, colour, index_start, index_end):
704
630
self.markers.append(('b', colour, str(index_start), str(index_end), \
956
867
Chart.__init__(self, *args, **kwargs)
957
868
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')
986
871
def type_to_url(self):
989
874
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)
1026
877
def get_url_bits(self, data_class=None):
1027
878
url_bits = Chart.get_url_bits(self, data_class=data_class)
1030
881
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())
1043
class GoogleOMeterChart(PieChart):
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.')
1052
def type_to_url(self):
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)
884
def annotated_data(self):
885
for dataset in self.data:
889
chart = PieChart2D(320, 200)
890
chart = ScatterChart(320, 200)
891
chart = SimpleLineChart(320, 200)
892
chart = GroupedVerticalBarChart(320, 200)
893
chart = SplineRadarChart(500, 500)
894
chart = MapChart(440, 220)
895
sine_data = [math.sin(float(a) / math.pi) * 100 for a in xrange(100)]
896
random_data = [random.random() * 100 for a in xrange(100)]
897
random_data2 = [random.random() * 50 for a in xrange(100)]
898
# chart.set_bar_width(50)
899
# chart.set_bar_spacing(0)
900
# chart.add_data(sine_data)
901
# chart.add_data(random_data)
902
# chart.add_data(random_data2)
903
# chart.set_line_style(0, thickness=5)
904
# chart.set_line_style(1, thickness=2, line_segment=10, blank_segment=5)
905
# chart.set_title('heloooo weeee')
906
# chart.set_legend(('sine wave', 'random * x'))
907
# chart.set_colours(('ee2000', 'DDDDAA', 'fF03f2'))
908
# chart.fill_solid(Chart.BACKGROUND, '123456')
909
# chart.fill_linear_gradient(Chart.CHART, 20, '004070', 1, '300040', 0,
911
# chart.fill_linear_stripes(Chart.CHART, 20, '204070', .2, '300040', .2,
913
# axis_left_index = chart.set_axis_range(Axis.LEFT, 0, 10)
914
# axis_right_index = chart.set_axis_range(Axis.RIGHT, 5, 30)
915
# axis_bottom_index = chart.set_axis_labels(Axis.BOTTOM, [1, 25, 95])
916
# chart.set_axis_positions(axis_bottom_index, [1, 25, 95])
917
# chart.set_axis_style(axis_bottom_index, '003050', 15)
919
# chart.set_pie_labels(('apples', 'oranges', 'bananas'))
921
# chart.set_grid(10, 10)
922
# for a in xrange(0, 100, 10):
923
# chart.add_marker(1, a, 'a', 'AACA20', 10)
925
# chart.add_horizontal_range('00A020', .2, .5)
926
# chart.add_vertical_range('00c030', .2, .4)
928
# chart.add_fill_simple('303030A0')
930
chart.set_codes(['AU', 'AT', 'US'])
931
chart.add_data([1,2,3])
932
chart.set_colours(('EEEEEE', '000000', '00FF00'))
933
url = chart.get_url()
936
chart.download('test.png')
939
data = urllib.urlopen(chart.get_url()).read()
940
open('meh.png', 'wb').write(data)
941
os.system('eog meh.png')
944
if __name__ == '__main__':