7
# Helper variables and functions
8
# -----------------------------------------------------------------------------
10
reo_colour = re.compile('^([A-Fa-f0-9]{2,2}){3,4}$')
12
def _check_colour(colour):
13
if not reo_colour.match(colour):
14
raise InvalidParametersException('Colours need to be in ' \
15
'RRGGBB or RRGGBBAA format. One of your colours has %s' % \
19
# -----------------------------------------------------------------------------
21
class PyGoogleChartException(Exception):
24
class DataOutOfRangeException(PyGoogleChartException):
27
class UnknownDataTypeException(PyGoogleChartException):
30
class NoDataGivenException(PyGoogleChartException):
33
class InvalidParametersException(PyGoogleChartException):
37
# -----------------------------------------------------------------------------
40
def __init__(self, data):
41
assert(type(self) != Data) # This is an abstract class
44
class SimpleData(Data):
45
enc_map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
48
for data in self.data:
53
elif value >= 0 and value <= SimpleData.max_value:
54
sub_data.append(SimpleData.enc_map[value])
56
raise DataOutOfRangeException()
57
encoded_data.append(''.join(sub_data))
58
return 'chd=s:' + ','.join(encoded_data)
66
for data in self.data:
71
elif value >= 0 and value <= TextData.max_value:
72
sub_data.append(str(float(value)))
74
raise DataOutOfRangeException()
75
encoded_data.append(','.join(sub_data))
76
return 'chd=t:' + '|'.join(encoded_data)
81
class ExtendedData(Data):
83
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-.'
86
enc_size = len(ExtendedData.enc_map)
87
for data in self.data:
92
elif value >= 0 and value <= ExtendedData.max_value:
93
first, second = divmod(int(value), enc_size)
94
sub_data.append('%s%s' % (
95
ExtendedData.enc_map[first],
96
ExtendedData.enc_map[second]))
98
raise DataOutOfRangeException( \
99
'Item #%i "%s" is out of range' % (data.index(value), \
101
encoded_data.append(''.join(sub_data))
102
return 'chd=e:' + ','.join(encoded_data)
108
# -----------------------------------------------------------------------------
115
TYPES = (BOTTOM, TOP, LEFT, RIGHT)
116
def __init__(self, axis, **kw):
117
assert(axis in Axis.TYPES)
118
self.has_style = False
120
self.positions = None
121
def set_index(self, index):
123
def set_positions(self, positions):
124
self.positions = positions
125
def set_style(self, colour, font_size=None, alignment=None):
126
_check_colour(colour)
128
self.font_size = font_size
129
self.alignment = alignment
130
self.has_style = True
131
def style_to_url(self):
133
bits.append(str(self.index))
134
bits.append(self.colour)
135
if self.font_size is not None:
136
bits.append(str(self.font_size))
137
if self.alignment is not None:
138
bits.append(str(self.alignment))
139
return ','.join(bits)
140
def positions_to_url(self):
142
bits.append(str(self.index))
143
bits += [ str(a) for a in self.positions ]
144
return ','.join(bits)
146
class LabelAxis(Axis):
147
def __init__(self, axis, values, **kwargs):
148
Axis.__init__(self, axis, **kwargs)
149
self.values = [ str(a) for a in values ]
151
return '%i:|%s' % (self.index, '|'.join(self.values))
153
class RangeAxis(Axis):
154
def __init__(self, axis, low, high, **kwargs):
155
Axis.__init__(self, axis, **kwargs)
159
return '%i,%s,%s' % (self.index, self.low, self.high)
162
# -----------------------------------------------------------------------------
166
BASE_URL = 'http://chart.apis.google.com/chart?'
170
LINEAR_GRADIENT = 'lg'
171
LINEAR_STRIPES = 'ls'
173
def __init__(self, width, height, title=None, legend=None, colours=None):
174
assert(type(self) != Chart) # This is an abstract class
175
assert(isinstance(width, int))
176
assert(isinstance(height, int))
180
self.set_title(title)
181
self.set_legend(legend)
182
self.set_colours(colours)
184
Chart.BACKGROUND: None,
188
Chart.BACKGROUND: None,
200
# -------------------------------------------------------------------------
203
url_bits = self.get_url_bits()
204
return self.BASE_URL + '&'.join(url_bits)
206
def get_url_bits(self):
209
url_bits.append(self.type_to_url())
210
url_bits.append('chs=%ix%i' % (self.width, self.height))
211
url_bits.append(self.data_to_url())
214
url_bits.append('chtt=%s' % self.title)
216
url_bits.append('chdl=%s' % '|'.join(self.legend))
218
url_bits.append('chco=%s' % ','.join(self.colours))
219
ret = self.fill_to_url()
222
ret = self.axis_to_url()
226
url_bits.append(self.markers_to_url())
230
# -------------------------------------------------------------------------
232
def set_title(self, title):
234
self.title = urllib.quote(title)
238
def set_legend(self, legend):
239
# legend needs to be a list, tuple or None
240
assert(isinstance(legend, list) or isinstance(legend, tuple) or
243
self.legend = [ urllib.quote(a) for a in legend ]
248
# -------------------------------------------------------------------------
250
def set_colours(self, colours):
251
# colours needs to be a list, tuple or None
252
assert(isinstance(colours, list) or isinstance(colours, tuple) or
254
# make sure the colours are in the right format
258
self.colours = colours
260
# Background/Chart colours
261
# -------------------------------------------------------------------------
263
def fill_solid(self, area, colour):
264
assert(area in (Chart.BACKGROUND, Chart.CHART))
265
_check_colour(colour)
266
self.fill_area[area] = colour
267
self.fill_types[area] = Chart.SOLID
269
def _check_fill_linear(self, angle, *args):
270
assert(isinstance(args, list) or isinstance(args, tuple))
271
assert(angle >= 0 and angle <= 90)
272
assert(len(args) % 2 == 0)
273
args = list(args) # args is probably a tuple and we need to mutate
274
for a in xrange(len(args) / 2):
276
offset = args[a * 2 + 1]
278
assert(offset >= 0 and offset <= 1)
279
args[a * 2 + 1] = str(args[a * 2 + 1])
282
def fill_linear_gradient(self, area, angle, *args):
283
assert(area in (Chart.BACKGROUND, Chart.CHART))
284
args = self._check_fill_linear(angle, *args)
285
self.fill_types[area] = Chart.LINEAR_GRADIENT
286
self.fill_area[area] = ','.join([str(angle)] + args)
288
def fill_linear_stripes(self, area, angle, *args):
289
assert(area in (Chart.BACKGROUND, Chart.CHART))
290
args = self._check_fill_linear(angle, *args)
291
self.fill_types[area] = Chart.LINEAR_STRIPES
292
self.fill_area[area] = ','.join([str(angle)] + args)
294
def fill_to_url(self):
296
for area in (Chart.BACKGROUND, Chart.CHART):
297
if self.fill_types[area]:
298
areas.append('%s,%s,%s' % (area, self.fill_types[area], \
299
self.fill_area[area]))
301
return 'chf=' + '|'.join(areas)
304
# -------------------------------------------------------------------------
306
def data_class_detection(self, data):
308
Detects and returns the data type required based on the range of the
309
data given. The data given must be lists of numbers within a list.
311
assert(isinstance(data, list) or isinstance(data, tuple))
314
assert(isinstance(a, list) or isinstance(a, tuple))
315
if max_value is None or max(a) > max_value:
317
for data_class in (SimpleData, TextData, ExtendedData):
318
if max_value <= data_class.max_value():
320
raise DataOutOfRangeException()
322
def add_data(self, data):
323
self.data.append(data)
325
def data_to_url(self, data_class=None):
327
data_class = self.data_class_detection(self.data)
328
if not issubclass(data_class, Data):
329
raise UnknownDataTypeException()
330
return repr(data_class(self.data))
333
# -------------------------------------------------------------------------
335
def set_axis_labels(self, axis, values):
336
assert(axis in Axis.TYPES)
337
self.axis[axis] = LabelAxis(axis, values)
339
def set_axis_range(self, axis, low, high):
340
assert(axis in Axis.TYPES)
341
self.axis[axis] = RangeAxis(axis, low, high)
343
def set_axis_positions(self, axis, positions):
344
assert(axis in Axis.TYPES)
345
if not self.axis[axis]:
346
raise InvalidParametersException('Please create an axis first')
347
self.axis[axis].set_positions(positions)
349
def set_axis_style(self, axis, colour, font_size=None, alignment=None):
350
assert(axis in Axis.TYPES)
351
if not self.axis[axis]:
352
raise InvalidParametersException('Please create an axis first')
353
self.axis[axis].set_style(colour, font_size, alignment)
355
def axis_to_url(self):
362
for position, axis in self.axis.items():
366
axis.set_index(index)
367
available_axis.append(position)
368
if isinstance(axis, RangeAxis):
369
range_axis.append(repr(axis))
370
if isinstance(axis, LabelAxis):
371
label_axis.append(repr(axis))
373
positions.append(axis.positions_to_url())
375
styles.append(axis.style_to_url())
376
if not available_axis:
379
url_bits.append('chxt=%s' % ','.join(available_axis))
381
url_bits.append('chxl=%s' % '|'.join(label_axis))
383
url_bits.append('chxr=%s' % '|'.join(range_axis))
385
url_bits.append('chxp=%s' % '|'.join(positions))
387
url_bits.append('chxs=%s' % '|'.join(styles))
388
return '&'.join(url_bits)
390
# Markers, Ranges and Fill area (chm)
391
# -------------------------------------------------------------------------
393
def markers_to_url(self):
394
print 'chm=%s' % '|'.join([ ','.join(a) for a in self.markers ])
395
return 'chm=%s' % '|'.join([ ','.join(a) for a in self.markers ])
397
def add_marker(self, index, point, marker_type, colour, size):
398
self.markers.append((marker_type, colour, str(index), str(point), \
401
def add_horizontal_range(self, colour, start, stop):
402
self.markers.append(('r', colour, '1', str(start), str(stop)))
404
def add_vertical_range(self, colour, start, stop):
405
self.markers.append(('R', colour, '1', str(start), str(stop)))
407
def add_fill_range(self, colour, index_start, index_end):
408
self.markers.append(('b', colour, str(index_start), str(index_end), \
411
def add_fill_simple(self, colour):
412
self.markers.append(('B', colour, '1', '1', '1'))
414
class ScatterChart(Chart):
415
def __init__(self, *args, **kwargs):
416
Chart.__init__(self, *args, **kwargs)
417
def type_to_url(self):
421
class LineChart(Chart):
422
def __init__(self, *args, **kwargs):
423
Chart.__init__(self, *args, **kwargs)
424
self.line_styles = {}
426
def set_line_style(self, index, thickness=1, line_segment=None, \
429
value.append(str(thickness))
431
value.append(str(line_segment))
432
value.append(str(blank_segment))
433
self.line_styles[index] = value
434
def set_grid(self, x_step, y_step, line_segment=1, \
436
self.grid = '%s,%s,%s,%s' % (x_step, y_step, line_segment, \
438
def get_url_bits(self):
439
url_bits = Chart.get_url_bits(self)
442
# for index, values in self.line_style.items():
443
for index in xrange(max(self.line_styles) + 1):
444
if index in self.line_styles:
445
values = self.line_styles[index]
448
style.append(','.join(values))
449
url_bits.append('chls=%s' % '|'.join(style))
451
url_bits.append('chg=%s' % self.grid)
454
class SimpleLineChart(LineChart):
455
def type_to_url(self):
458
class XYLineChart(LineChart):
459
def type_to_url(self):
462
class BarChart(Chart):
463
def __init__(self, *args, **kwargs):
464
assert(type(self) != BarChart) # This is an abstract class
465
Chart.__init__(self, *args, **kwargs)
466
self.bar_width = None
467
def set_bar_width(self, bar_width):
468
self.bar_width = bar_width
469
def get_url_bits(self):
470
url_bits = Chart.get_url_bits(self)
471
url_bits.append('chbh=%i' % self.bar_width)
474
class StackedHorizontalBarChart(BarChart):
475
def type_to_url(self):
478
class StackedVerticalBarChart(BarChart):
479
def type_to_url(self):
482
class GroupedBarChart(BarChart):
483
def __init__(self, *args, **kwargs):
484
assert(type(self) != GroupedBarChart) # This is an abstract class
485
BarChart.__init__(self, *args, **kwargs)
486
self.bar_spacing = None
487
def set_bar_spacing(self, spacing):
488
self.bar_spacing = spacing
489
def get_url_bits(self):
490
# Skip 'BarChart.get_url_bits' and call Chart directly so the parent
491
# doesn't add "chbh" before we do.
492
url_bits = Chart.get_url_bits(self)
493
if self.bar_spacing is not None:
494
if self.bar_width is None:
495
raise InvalidParametersException('Bar width is required to ' \
496
'be set when setting spacing')
497
url_bits.append('chbh=%i,%i' % (self.bar_width, self.bar_spacing))
499
url_bits.append('chbh=%i' % self.bar_width)
502
class GroupedHorizontalBarChart(GroupedBarChart):
503
def type_to_url(self):
506
class GroupedVerticalBarChart(GroupedBarChart):
507
def type_to_url(self):
510
class PieChart(Chart):
511
def __init__(self, *args, **kwargs):
512
assert(type(self) != PieChart) # This is an abstract class
513
Chart.__init__(self, *args, **kwargs)
515
def set_pie_labels(self, labels):
516
self.pie_labels = labels
517
def get_url_bits(self):
518
url_bits = Chart.get_url_bits(self)
520
url_bits.append('chl=%s' % '|'.join(self.pie_labels))
523
class PieChart2D(PieChart):
524
def type_to_url(self):
527
class PieChart3D(PieChart):
528
def type_to_url(self):
531
class VennChart(Chart):
532
def type_to_url(self):
536
chart = GroupedVerticalBarChart(320, 200)
537
chart = PieChart2D(320, 200)
538
chart = ScatterChart(320, 200)
539
chart = SimpleLineChart(320, 200)
540
sine_data = [ math.sin(float(a) / 10) * 2000 + 2000 for a in xrange(100) ]
541
random_data = [ a * random.random() * 30 for a in xrange(40) ]
542
random_data2 = [ random.random() * 4000 for a in xrange(10) ]
543
# chart.set_bar_width(50)
544
# chart.set_bar_spacing(0)
545
chart.add_data(sine_data)
546
chart.add_data(random_data)
547
chart.add_data(random_data2)
548
# chart.set_line_style(1, thickness=2)
549
# chart.set_line_style(2, line_segment=10, blank_segment=5)
550
# chart.set_title('heloooo')
551
# chart.set_legend(('sine wave', 'random * x'))
552
# chart.set_colours(('ee2000', 'DDDDAA', 'fF03f2'))
553
# chart.fill_solid(Chart.BACKGROUND, '123456')
554
# chart.fill_linear_gradient(Chart.CHART, 20, '004070', 1, '300040', 0,
556
# chart.fill_linear_stripes(Chart.CHART, 20, '204070', .2, '300040', .2,
558
chart.set_axis_range(Axis.LEFT, 0, 10)
559
chart.set_axis_range(Axis.RIGHT, 5, 30)
560
chart.set_axis_labels(Axis.BOTTOM, [1, 25, 95])
561
chart.set_axis_positions(Axis.BOTTOM, [1, 25, 95])
562
chart.set_axis_style(Axis.BOTTOM, 'FFFFFF', 15)
564
# chart.set_pie_labels(('apples', 'oranges', 'bananas'))
566
# chart.set_grid(10, 10)
568
# for a in xrange(0, 100, 10):
569
# chart.add_marker(1, a, 'a', 'AACA20', 10)
571
chart.add_horizontal_range('00A020', .2, .5)
572
chart.add_vertical_range('00c030', .2, .4)
574
chart.add_fill_simple('303030A0')
576
url = chart.get_url()
579
data = urllib.urlopen(chart.get_url()).read()
580
open('meh.png', 'wb').write(data)
581
os.system('start meh.png')
583
if __name__ == '__main__':