2
PyGoogleChart - A complete Python wrapper for the Google Chart API
4
http://pygooglechart.slowchop.com/
6
Copyright 2007 Gerald Kaszuba
8
This program is free software: you can redistribute it and/or modify
9
it under the terms of the GNU General Public License as published by
10
the Free Software Foundation, either version 3 of the License, or
11
(at your option) any later version.
13
This program is distributed in the hope that it will be useful,
14
but WITHOUT ANY WARRANTY; without even the implied warranty of
15
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
GNU General Public License for more details.
18
You should have received a copy of the GNU General Public License
19
along with this program. If not, see <http://www.gnu.org/licenses/>.
30
# Helper variables and functions
31
# -----------------------------------------------------------------------------
35
reo_colour = re.compile('^([A-Fa-f0-9]{2,2}){3,4}$')
38
def _check_colour(colour):
39
if not reo_colour.match(colour):
40
raise InvalidParametersException('Colours need to be in ' \
41
'RRGGBB or RRGGBBAA format. One of your colours has %s' % \
45
# -----------------------------------------------------------------------------
48
class PyGoogleChartException(Exception):
52
class DataOutOfRangeException(PyGoogleChartException):
56
class UnknownDataTypeException(PyGoogleChartException):
60
class NoDataGivenException(PyGoogleChartException):
64
class InvalidParametersException(PyGoogleChartException):
68
class BadContentTypeException(PyGoogleChartException):
73
# -----------------------------------------------------------------------------
78
def __init__(self, data):
79
assert(type(self) != Data) # This is an abstract class
83
class SimpleData(Data):
84
enc_map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
88
for data in self.data:
93
elif value >= 0 and value <= SimpleData.max_value:
94
sub_data.append(SimpleData.enc_map[value])
96
raise DataOutOfRangeException()
97
encoded_data.append(''.join(sub_data))
98
return 'chd=s:' + ','.join(encoded_data)
105
class TextData(Data):
109
for data in self.data:
114
elif value >= 0 and value <= TextData.max_value:
115
sub_data.append(str(float(value)))
117
raise DataOutOfRangeException()
118
encoded_data.append(','.join(sub_data))
119
return 'chd=t:' + '|'.join(encoded_data)
126
class ExtendedData(Data):
128
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-.'
132
enc_size = len(ExtendedData.enc_map)
133
for data in self.data:
137
sub_data.append('__')
138
elif value >= 0 and value <= ExtendedData.max_value:
139
first, second = divmod(int(value), enc_size)
140
sub_data.append('%s%s' % (
141
ExtendedData.enc_map[first],
142
ExtendedData.enc_map[second]))
144
raise DataOutOfRangeException( \
145
'Item #%i "%s" is out of range' % (data.index(value), \
147
encoded_data.append(''.join(sub_data))
148
return 'chd=e:' + ','.join(encoded_data)
155
# -----------------------------------------------------------------------------
163
TYPES = (BOTTOM, TOP, LEFT, RIGHT)
165
def __init__(self, axis_index, axis_type, **kw):
166
assert(axis_type in Axis.TYPES)
167
self.has_style = False
168
self.axis_index = axis_index
169
self.axis_type = axis_type
170
self.positions = None
172
def set_index(self, axis_index):
173
self.axis_index = axis_index
175
def set_positions(self, positions):
176
self.positions = positions
178
def set_style(self, colour, font_size=None, alignment=None):
179
_check_colour(colour)
181
self.font_size = font_size
182
self.alignment = alignment
183
self.has_style = True
185
def style_to_url(self):
187
bits.append(str(self.axis_index))
188
bits.append(self.colour)
189
if self.font_size is not None:
190
bits.append(str(self.font_size))
191
if self.alignment is not None:
192
bits.append(str(self.alignment))
193
return ','.join(bits)
195
def positions_to_url(self):
197
bits.append(str(self.axis_index))
198
bits += [str(a) for a in self.positions]
199
return ','.join(bits)
202
class LabelAxis(Axis):
204
def __init__(self, axis_index, axis_type, values, **kwargs):
205
Axis.__init__(self, axis_index, axis_type, **kwargs)
206
self.values = [str(a) for a in values]
209
return '%i:|%s' % (self.axis_index, '|'.join(self.values))
212
class RangeAxis(Axis):
214
def __init__(self, axis_index, axis_type, low, high, **kwargs):
215
Axis.__init__(self, axis_index, axis_type, **kwargs)
220
return '%i,%s,%s' % (self.axis_index, self.low, self.high)
223
# -----------------------------------------------------------------------------
227
"""Abstract class for all chart types.
229
width are height specify the dimensions of the image. title sets the title
230
of the chart. legend requires a list that corresponds to datasets.
233
BASE_URL = 'http://chart.apis.google.com/chart?'
237
LINEAR_GRADIENT = 'lg'
238
LINEAR_STRIPES = 'ls'
240
def __init__(self, width, height, title=None, legend=None, colours=None):
241
assert(type(self) != Chart) # This is an abstract class
242
assert(isinstance(width, int))
243
assert(isinstance(height, int))
247
self.set_title(title)
248
self.set_legend(legend)
249
self.set_colours(colours)
251
Chart.BACKGROUND: None,
255
Chart.BACKGROUND: None,
268
# -------------------------------------------------------------------------
271
url_bits = self.get_url_bits()
272
return self.BASE_URL + '&'.join(url_bits)
274
def get_url_bits(self):
277
url_bits.append(self.type_to_url())
278
url_bits.append('chs=%ix%i' % (self.width, self.height))
279
url_bits.append(self.data_to_url())
282
url_bits.append('chtt=%s' % self.title)
284
url_bits.append('chdl=%s' % '|'.join(self.legend))
286
url_bits.append('chco=%s' % ','.join(self.colours))
287
ret = self.fill_to_url()
290
ret = self.axis_to_url()
294
url_bits.append(self.markers_to_url())
298
# -------------------------------------------------------------------------
300
def download(self, file_name):
301
opener = urllib2.urlopen(self.get_url())
303
if opener.headers['content-type'] != 'image/png':
304
raise BadContentTypeException('Server responded with a ' \
305
'content-type of %s' % opener.headers['content-type'])
307
open(file_name, 'wb').write(urllib.urlopen(self.get_url()).read())
310
# -------------------------------------------------------------------------
312
def set_title(self, title):
314
self.title = urllib.quote(title)
318
def set_legend(self, legend):
319
# legend needs to be a list, tuple or None
320
assert(isinstance(legend, list) or isinstance(legend, tuple) or
323
self.legend = [urllib.quote(a) for a in legend]
328
# -------------------------------------------------------------------------
330
def set_colours(self, colours):
331
# colours needs to be a list, tuple or None
332
assert(isinstance(colours, list) or isinstance(colours, tuple) or
334
# make sure the colours are in the right format
338
self.colours = colours
340
# Background/Chart colours
341
# -------------------------------------------------------------------------
343
def fill_solid(self, area, colour):
344
assert(area in (Chart.BACKGROUND, Chart.CHART))
345
_check_colour(colour)
346
self.fill_area[area] = colour
347
self.fill_types[area] = Chart.SOLID
349
def _check_fill_linear(self, angle, *args):
350
assert(isinstance(args, list) or isinstance(args, tuple))
351
assert(angle >= 0 and angle <= 90)
352
assert(len(args) % 2 == 0)
353
args = list(args) # args is probably a tuple and we need to mutate
354
for a in xrange(len(args) / 2):
356
offset = args[a * 2 + 1]
358
assert(offset >= 0 and offset <= 1)
359
args[a * 2 + 1] = str(args[a * 2 + 1])
362
def fill_linear_gradient(self, area, angle, *args):
363
assert(area in (Chart.BACKGROUND, Chart.CHART))
364
args = self._check_fill_linear(angle, *args)
365
self.fill_types[area] = Chart.LINEAR_GRADIENT
366
self.fill_area[area] = ','.join([str(angle)] + args)
368
def fill_linear_stripes(self, area, angle, *args):
369
assert(area in (Chart.BACKGROUND, Chart.CHART))
370
args = self._check_fill_linear(angle, *args)
371
self.fill_types[area] = Chart.LINEAR_STRIPES
372
self.fill_area[area] = ','.join([str(angle)] + args)
374
def fill_to_url(self):
376
for area in (Chart.BACKGROUND, Chart.CHART):
377
if self.fill_types[area]:
378
areas.append('%s,%s,%s' % (area, self.fill_types[area], \
379
self.fill_area[area]))
381
return 'chf=' + '|'.join(areas)
384
# -------------------------------------------------------------------------
386
def data_class_detection(self, data):
388
Detects and returns the data type required based on the range of the
389
data given. The data given must be lists of numbers within a list.
391
assert(isinstance(data, list) or isinstance(data, tuple))
394
assert(isinstance(a, list) or isinstance(a, tuple))
395
if max_value is None or max(a) > max_value:
397
for data_class in (SimpleData, TextData, ExtendedData):
398
if max_value <= data_class.max_value():
400
raise DataOutOfRangeException()
402
def add_data(self, data):
403
self.data.append(data)
404
return len(self.data) - 1 # return the "index" of the data set
406
def data_to_url(self, data_class=None):
408
data_class = self.data_class_detection(self.data)
409
if not issubclass(data_class, Data):
410
raise UnknownDataTypeException()
411
return repr(data_class(self.data))
414
# -------------------------------------------------------------------------
416
def set_axis_labels(self, axis_type, values):
417
assert(axis_type in Axis.TYPES)
418
values = [ urllib.quote(a) for a in values ]
419
axis_index = len(self.axis)
420
axis = LabelAxis(axis_index, axis_type, values)
421
self.axis.append(axis)
424
def set_axis_range(self, axis_type, low, high):
425
assert(axis_type in Axis.TYPES)
426
axis_index = len(self.axis)
427
axis = RangeAxis(axis_index, axis_type, low, high)
428
self.axis.append(axis)
431
def set_axis_positions(self, axis_index, positions):
433
self.axis[axis_index].set_positions(positions)
435
raise InvalidParametersException('Axis index %i has not been ' \
438
def set_axis_style(self, axis_index, colour, font_size=None, \
441
self.axis[axis_index].set_style(colour, font_size, alignment)
443
raise InvalidParametersException('Axis index %i has not been ' \
446
def axis_to_url(self):
453
for axis in self.axis:
454
available_axis.append(axis.axis_type)
455
if isinstance(axis, RangeAxis):
456
range_axis.append(repr(axis))
457
if isinstance(axis, LabelAxis):
458
label_axis.append(repr(axis))
460
positions.append(axis.positions_to_url())
462
styles.append(axis.style_to_url())
463
if not available_axis:
466
url_bits.append('chxt=%s' % ','.join(available_axis))
468
url_bits.append('chxl=%s' % '|'.join(label_axis))
470
url_bits.append('chxr=%s' % '|'.join(range_axis))
472
url_bits.append('chxp=%s' % '|'.join(positions))
474
url_bits.append('chxs=%s' % '|'.join(styles))
475
return '&'.join(url_bits)
477
# Markers, Ranges and Fill area (chm)
478
# -------------------------------------------------------------------------
480
def markers_to_url(self):
481
return 'chm=%s' % '|'.join([','.join(a) for a in self.markers])
483
def add_marker(self, index, point, marker_type, colour, size):
484
self.markers.append((marker_type, colour, str(index), str(point), \
487
def add_horizontal_range(self, colour, start, stop):
488
self.markers.append(('r', colour, '1', str(start), str(stop)))
490
def add_vertical_range(self, colour, start, stop):
491
self.markers.append(('R', colour, '1', str(start), str(stop)))
493
def add_fill_range(self, colour, index_start, index_end):
494
self.markers.append(('b', colour, str(index_start), str(index_end), \
497
def add_fill_simple(self, colour):
498
self.markers.append(('B', colour, '1', '1', '1'))
501
class ScatterChart(Chart):
503
def __init__(self, *args, **kwargs):
504
Chart.__init__(self, *args, **kwargs)
506
def type_to_url(self):
510
class LineChart(Chart):
512
def __init__(self, *args, **kwargs):
513
assert(type(self) != LineChart) # This is an abstract class
514
Chart.__init__(self, *args, **kwargs)
515
self.line_styles = {}
518
def set_line_style(self, index, thickness=1, line_segment=None, \
521
value.append(str(thickness))
523
value.append(str(line_segment))
524
value.append(str(blank_segment))
525
self.line_styles[index] = value
527
def set_grid(self, x_step, y_step, line_segment=1, \
529
self.grid = '%s,%s,%s,%s' % (x_step, y_step, line_segment, \
532
def get_url_bits(self):
533
url_bits = Chart.get_url_bits(self)
536
# for index, values in self.line_style.items():
537
for index in xrange(max(self.line_styles) + 1):
538
if index in self.line_styles:
539
values = self.line_styles[index]
542
style.append(','.join(values))
543
url_bits.append('chls=%s' % '|'.join(style))
545
url_bits.append('chg=%s' % self.grid)
549
class SimpleLineChart(LineChart):
551
def type_to_url(self):
555
class XYLineChart(LineChart):
557
def type_to_url(self):
561
class BarChart(Chart):
563
def __init__(self, *args, **kwargs):
564
assert(type(self) != BarChart) # This is an abstract class
565
Chart.__init__(self, *args, **kwargs)
566
self.bar_width = None
568
def set_bar_width(self, bar_width):
569
self.bar_width = bar_width
571
def get_url_bits(self):
572
url_bits = Chart.get_url_bits(self)
573
url_bits.append('chbh=%i' % self.bar_width)
577
class StackedHorizontalBarChart(BarChart):
579
def type_to_url(self):
583
class StackedVerticalBarChart(BarChart):
585
def type_to_url(self):
589
class GroupedBarChart(BarChart):
591
def __init__(self, *args, **kwargs):
592
assert(type(self) != GroupedBarChart) # This is an abstract class
593
BarChart.__init__(self, *args, **kwargs)
594
self.bar_spacing = None
596
def set_bar_spacing(self, spacing):
597
self.bar_spacing = spacing
599
def get_url_bits(self):
600
# Skip 'BarChart.get_url_bits' and call Chart directly so the parent
601
# doesn't add "chbh" before we do.
602
url_bits = Chart.get_url_bits(self)
603
if self.bar_spacing is not None:
604
if self.bar_width is None:
605
raise InvalidParametersException('Bar width is required to ' \
606
'be set when setting spacing')
607
url_bits.append('chbh=%i,%i' % (self.bar_width, self.bar_spacing))
609
url_bits.append('chbh=%i' % self.bar_width)
613
class GroupedHorizontalBarChart(GroupedBarChart):
615
def type_to_url(self):
619
class GroupedVerticalBarChart(GroupedBarChart):
621
def type_to_url(self):
625
class PieChart(Chart):
627
def __init__(self, *args, **kwargs):
628
assert(type(self) != PieChart) # This is an abstract class
629
Chart.__init__(self, *args, **kwargs)
632
def set_pie_labels(self, labels):
633
self.pie_labels = labels
635
def get_url_bits(self):
636
url_bits = Chart.get_url_bits(self)
638
url_bits.append('chl=%s' % '|'.join(self.pie_labels))
642
class PieChart2D(PieChart):
644
def type_to_url(self):
648
class PieChart3D(PieChart):
650
def type_to_url(self):
654
class VennChart(Chart):
656
def type_to_url(self):
661
chart = GroupedVerticalBarChart(320, 200)
662
chart = PieChart2D(320, 200)
663
chart = ScatterChart(320, 200)
664
chart = SimpleLineChart(320, 200)
665
sine_data = [math.sin(float(a) / 10) * 2000 + 2000 for a in xrange(100)]
666
random_data = [a * random.random() * 30 for a in xrange(40)]
667
random_data2 = [random.random() * 4000 for a in xrange(10)]
668
# chart.set_bar_width(50)
669
# chart.set_bar_spacing(0)
670
chart.add_data(sine_data)
671
chart.add_data(random_data)
672
chart.add_data(random_data2)
673
# chart.set_line_style(1, thickness=2)
674
# chart.set_line_style(2, line_segment=10, blank_segment=5)
675
# chart.set_title('heloooo')
676
# chart.set_legend(('sine wave', 'random * x'))
677
# chart.set_colours(('ee2000', 'DDDDAA', 'fF03f2'))
678
# chart.fill_solid(Chart.BACKGROUND, '123456')
679
# chart.fill_linear_gradient(Chart.CHART, 20, '004070', 1, '300040', 0,
681
# chart.fill_linear_stripes(Chart.CHART, 20, '204070', .2, '300040', .2,
683
axis_left_index = chart.set_axis_range(Axis.LEFT, 0, 10)
684
axis_left_index = chart.set_axis_range(Axis.LEFT, 0, 10)
685
axis_left_index = chart.set_axis_range(Axis.LEFT, 0, 10)
686
axis_right_index = chart.set_axis_range(Axis.RIGHT, 5, 30)
687
axis_bottom_index = chart.set_axis_labels(Axis.BOTTOM, [1, 25, 95])
688
chart.set_axis_positions(axis_bottom_index, [1, 25, 95])
689
chart.set_axis_style(axis_bottom_index, '003050', 15)
691
# chart.set_pie_labels(('apples', 'oranges', 'bananas'))
693
# chart.set_grid(10, 10)
695
# for a in xrange(0, 100, 10):
696
# chart.add_marker(1, a, 'a', 'AACA20', 10)
698
chart.add_horizontal_range('00A020', .2, .5)
699
chart.add_vertical_range('00c030', .2, .4)
701
chart.add_fill_simple('303030A0')
703
chart.download('test.png')
705
url = chart.get_url()
708
data = urllib.urlopen(chart.get_url()).read()
709
open('meh.png', 'wb').write(data)
710
os.system('start meh.png')
713
if __name__ == '__main__':
2
PyGoogleChart - A complete Python wrapper for the Google Chart API
4
http://pygooglechart.slowchop.com/
6
Copyright 2007-2008 Gerald Kaszuba
8
This program is free software: you can redistribute it and/or modify
9
it under the terms of the GNU General Public License as published by
10
the Free Software Foundation, either version 3 of the License, or
11
(at your option) any later version.
13
This program is distributed in the hope that it will be useful,
14
but WITHOUT ANY WARRANTY; without even the implied warranty of
15
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
GNU General Public License for more details.
18
You should have received a copy of the GNU General Public License
19
along with this program. If not, see <http://www.gnu.org/licenses/>.
30
# Helper variables and functions
31
# -----------------------------------------------------------------------------
35
reo_colour = re.compile('^([A-Fa-f0-9]{2,2}){3,4}$')
38
def _check_colour(colour):
39
if not reo_colour.match(colour):
40
raise InvalidParametersException('Colours need to be in ' \
41
'RRGGBB or RRGGBBAA format. One of your colours has %s' % \
45
# -----------------------------------------------------------------------------
48
class PyGoogleChartException(Exception):
52
class DataOutOfRangeException(PyGoogleChartException):
56
class UnknownDataTypeException(PyGoogleChartException):
60
class NoDataGivenException(PyGoogleChartException):
64
class InvalidParametersException(PyGoogleChartException):
68
class BadContentTypeException(PyGoogleChartException):
73
# -----------------------------------------------------------------------------
78
def __init__(self, data):
79
assert(type(self) != Data) # This is an abstract class
83
def float_scale_value(cls, value, range):
86
max_value = cls.max_value()
87
scaled = (value-lower) * (float(max_value) / (upper - lower))
91
def clip_value(cls, value):
92
return max(0, min(value, cls.max_value()))
95
def int_scale_value(cls, value, range):
96
return int(round(cls.float_scale_value(value, range)))
99
def scale_value(cls, value, range):
100
scaled = cls.int_scale_value(value, range)
101
clipped = cls.clip_value(scaled)
105
class SimpleData(Data):
107
enc_map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
110
max_value = self.max_value()
112
for data in self.data:
117
elif value >= 0 and value <= max_value:
118
sub_data.append(SimpleData.enc_map[value])
120
raise DataOutOfRangeException('cannot encode value: %d'
122
encoded_data.append(''.join(sub_data))
123
return 'chd=s:' + ','.join(encoded_data)
130
class TextData(Data):
133
max_value = self.max_value()
135
for data in self.data:
140
elif value >= 0 and value <= max_value:
141
sub_data.append("%.1f" % float(value))
143
raise DataOutOfRangeException()
144
encoded_data.append(','.join(sub_data))
145
return 'chd=t:' + '|'.join(encoded_data)
152
def scale_value(cls, value, range):
153
# use float values instead of integers because we don't need an encode
155
scaled = cls.float_scale_value(value, range)
156
clipped = cls.clip_value(scaled)
160
class ExtendedData(Data):
163
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-.'
166
max_value = self.max_value()
168
enc_size = len(ExtendedData.enc_map)
169
for data in self.data:
173
sub_data.append('__')
174
elif value >= 0 and value <= max_value:
175
first, second = divmod(int(value), enc_size)
176
sub_data.append('%s%s' % (
177
ExtendedData.enc_map[first],
178
ExtendedData.enc_map[second]))
180
raise DataOutOfRangeException( \
181
'Item #%i "%s" is out of range' % (data.index(value), \
183
encoded_data.append(''.join(sub_data))
184
return 'chd=e:' + ','.join(encoded_data)
192
# -----------------------------------------------------------------------------
201
TYPES = (BOTTOM, TOP, LEFT, RIGHT)
203
def __init__(self, axis_index, axis_type, **kw):
204
assert(axis_type in Axis.TYPES)
205
self.has_style = False
206
self.axis_index = axis_index
207
self.axis_type = axis_type
208
self.positions = None
210
def set_index(self, axis_index):
211
self.axis_index = axis_index
213
def set_positions(self, positions):
214
self.positions = positions
216
def set_style(self, colour, font_size=None, alignment=None):
217
_check_colour(colour)
219
self.font_size = font_size
220
self.alignment = alignment
221
self.has_style = True
223
def style_to_url(self):
225
bits.append(str(self.axis_index))
226
bits.append(self.colour)
227
if self.font_size is not None:
228
bits.append(str(self.font_size))
229
if self.alignment is not None:
230
bits.append(str(self.alignment))
231
return ','.join(bits)
233
def positions_to_url(self):
235
bits.append(str(self.axis_index))
236
bits += [str(a) for a in self.positions]
237
return ','.join(bits)
240
class LabelAxis(Axis):
242
def __init__(self, axis_index, axis_type, values, **kwargs):
243
Axis.__init__(self, axis_index, axis_type, **kwargs)
244
self.values = [str(a) for a in values]
247
return '%i:|%s' % (self.axis_index, '|'.join(self.values))
250
class RangeAxis(Axis):
252
def __init__(self, axis_index, axis_type, low, high, **kwargs):
253
Axis.__init__(self, axis_index, axis_type, **kwargs)
258
return '%i,%s,%s' % (self.axis_index, self.low, self.high)
261
# -----------------------------------------------------------------------------
265
"""Abstract class for all chart types.
267
width are height specify the dimensions of the image. title sets the title
268
of the chart. legend requires a list that corresponds to datasets.
271
BASE_URL = 'http://chart.apis.google.com/chart?'
275
VALID_SOLID_FILL_TYPES = (BACKGROUND, CHART, ALPHA)
277
LINEAR_GRADIENT = 'lg'
278
LINEAR_STRIPES = 'ls'
280
def __init__(self, width, height, title=None, legend=None, colours=None,
281
auto_scale=True, x_range=None, y_range=None):
282
assert(type(self) != Chart) # This is an abstract class
283
assert(isinstance(width, int))
284
assert(isinstance(height, int))
288
self.set_title(title)
289
self.set_legend(legend)
290
self.set_colours(colours)
293
self.auto_scale = auto_scale # Whether to automatically scale data
294
self.x_range = x_range # (min, max) x-axis range for scaling
295
self.y_range = y_range # (min, max) y-axis range for scaling
296
self.scaled_data_class = None
297
self.scaled_x_range = None
298
self.scaled_y_range = None
301
Chart.BACKGROUND: None,
306
Chart.BACKGROUND: None,
312
self.line_styles = {}
316
# -------------------------------------------------------------------------
318
def get_url(self, data_class=None):
319
url_bits = self.get_url_bits(data_class=data_class)
320
return self.BASE_URL + '&'.join(url_bits)
322
def get_url_bits(self, data_class=None):
325
url_bits.append(self.type_to_url())
326
url_bits.append('chs=%ix%i' % (self.width, self.height))
327
url_bits.append(self.data_to_url(data_class=data_class))
330
url_bits.append('chtt=%s' % self.title)
332
url_bits.append('chdl=%s' % '|'.join(self.legend))
334
url_bits.append('chco=%s' % ','.join(self.colours))
335
ret = self.fill_to_url()
338
ret = self.axis_to_url()
342
url_bits.append(self.markers_to_url())
345
for index in xrange(max(self.line_styles) + 1):
346
if index in self.line_styles:
347
values = self.line_styles[index]
350
style.append(','.join(values))
351
url_bits.append('chls=%s' % '|'.join(style))
353
url_bits.append('chg=%s' % self.grid)
357
# -------------------------------------------------------------------------
359
def download(self, file_name):
360
opener = urllib2.urlopen(self.get_url())
362
if opener.headers['content-type'] != 'image/png':
363
raise BadContentTypeException('Server responded with a ' \
364
'content-type of %s' % opener.headers['content-type'])
366
open(file_name, 'wb').write(urllib.urlopen(self.get_url()).read())
369
# -------------------------------------------------------------------------
371
def set_title(self, title):
373
self.title = urllib.quote(title)
377
def set_legend(self, legend):
378
"""legend needs to be a list, tuple or None"""
379
assert(isinstance(legend, list) or isinstance(legend, tuple) or
382
self.legend = [urllib.quote(a) for a in legend]
387
# -------------------------------------------------------------------------
389
def set_colours(self, colours):
390
# colours needs to be a list, tuple or None
391
assert(isinstance(colours, list) or isinstance(colours, tuple) or
393
# make sure the colours are in the right format
397
self.colours = colours
399
# Background/Chart colours
400
# -------------------------------------------------------------------------
402
def fill_solid(self, area, colour):
403
assert(area in Chart.VALID_SOLID_FILL_TYPES)
404
_check_colour(colour)
405
self.fill_area[area] = colour
406
self.fill_types[area] = Chart.SOLID
408
def _check_fill_linear(self, angle, *args):
409
assert(isinstance(args, list) or isinstance(args, tuple))
410
assert(angle >= 0 and angle <= 90)
411
assert(len(args) % 2 == 0)
412
args = list(args) # args is probably a tuple and we need to mutate
413
for a in xrange(len(args) / 2):
415
offset = args[a * 2 + 1]
417
assert(offset >= 0 and offset <= 1)
418
args[a * 2 + 1] = str(args[a * 2 + 1])
421
def fill_linear_gradient(self, area, angle, *args):
422
assert(area in Chart.VALID_SOLID_FILL_TYPES)
423
args = self._check_fill_linear(angle, *args)
424
self.fill_types[area] = Chart.LINEAR_GRADIENT
425
self.fill_area[area] = ','.join([str(angle)] + args)
427
def fill_linear_stripes(self, area, angle, *args):
428
assert(area in Chart.VALID_SOLID_FILL_TYPES)
429
args = self._check_fill_linear(angle, *args)
430
self.fill_types[area] = Chart.LINEAR_STRIPES
431
self.fill_area[area] = ','.join([str(angle)] + args)
433
def fill_to_url(self):
435
for area in (Chart.BACKGROUND, Chart.CHART, Chart.ALPHA):
436
if self.fill_types[area]:
437
areas.append('%s,%s,%s' % (area, self.fill_types[area], \
438
self.fill_area[area]))
440
return 'chf=' + '|'.join(areas)
443
# -------------------------------------------------------------------------
445
def data_class_detection(self, data):
446
"""Determines the appropriate data encoding type to give satisfactory
447
resolution (http://code.google.com/apis/chart/#chart_data).
449
assert(isinstance(data, list) or isinstance(data, tuple))
450
if not isinstance(self, (LineChart, BarChart, ScatterChart)):
451
# From the link above:
452
# Simple encoding is suitable for all other types of chart
453
# regardless of size.
455
elif self.height < 100:
456
# The link above indicates that line and bar charts less
457
# than 300px in size can be suitably represented with the
458
# simple encoding. I've found that this isn't sufficient,
459
# e.g. examples/line-xy-circle.png. Let's try 100px.
464
def data_x_range(self):
465
"""Return a 2-tuple giving the minimum and maximum x-axis
469
lower = min([min(s) for type, s in self.annotated_data()
471
upper = max([max(s) for type, s in self.annotated_data()
473
return (lower, upper)
475
return None # no x-axis datasets
477
def data_y_range(self):
478
"""Return a 2-tuple giving the minimum and maximum y-axis
482
lower = min([min(s) for type, s in self.annotated_data()
484
upper = max([max(s) + 1 for type, s in self.annotated_data()
486
return (lower, upper)
488
return None # no y-axis datasets
490
def scaled_data(self, data_class, x_range=None, y_range=None):
491
"""Scale `self.data` as appropriate for the given data encoding
492
(data_class) and return it.
494
An optional `y_range` -- a 2-tuple (lower, upper) -- can be
495
given to specify the y-axis bounds. If not given, the range is
496
inferred from the data: (0, <max-value>) presuming no negative
497
values, or (<min-value>, <max-value>) if there are negative
498
values. `self.scaled_y_range` is set to the actual lower and
501
Ditto for `x_range`. Note that some chart types don't have x-axis
504
self.scaled_data_class = data_class
506
# Determine the x-axis range for scaling.
508
x_range = self.data_x_range()
509
if x_range and x_range[0] > 0:
510
x_range = (0, x_range[1])
511
self.scaled_x_range = x_range
513
# Determine the y-axis range for scaling.
515
y_range = self.data_y_range()
516
if y_range and y_range[0] > 0:
517
y_range = (0, y_range[1])
518
self.scaled_y_range = y_range
521
for type, dataset in self.annotated_data():
523
scale_range = x_range
525
scale_range = y_range
526
elif type == 'marker-size':
527
scale_range = (0, max(dataset))
528
scaled_data.append([data_class.scale_value(v, scale_range)
532
def add_data(self, data):
533
self.data.append(data)
534
return len(self.data) - 1 # return the "index" of the data set
536
def data_to_url(self, data_class=None):
538
data_class = self.data_class_detection(self.data)
539
if not issubclass(data_class, Data):
540
raise UnknownDataTypeException()
543
data = self.scaled_data(data_class, self.x_range, self.y_range)
546
return repr(data_class(data))
548
def annotated_data(self):
549
for dataset in self.data:
553
# -------------------------------------------------------------------------
555
def set_axis_labels(self, axis_type, values):
556
assert(axis_type in Axis.TYPES)
557
values = [urllib.quote(a) for a in values]
558
axis_index = len(self.axis)
559
axis = LabelAxis(axis_index, axis_type, values)
560
self.axis.append(axis)
563
def set_axis_range(self, axis_type, low, high):
564
assert(axis_type in Axis.TYPES)
565
axis_index = len(self.axis)
566
axis = RangeAxis(axis_index, axis_type, low, high)
567
self.axis.append(axis)
570
def set_axis_positions(self, axis_index, positions):
572
self.axis[axis_index].set_positions(positions)
574
raise InvalidParametersException('Axis index %i has not been ' \
577
def set_axis_style(self, axis_index, colour, font_size=None, \
580
self.axis[axis_index].set_style(colour, font_size, alignment)
582
raise InvalidParametersException('Axis index %i has not been ' \
585
def axis_to_url(self):
592
for axis in self.axis:
593
available_axis.append(axis.axis_type)
594
if isinstance(axis, RangeAxis):
595
range_axis.append(repr(axis))
596
if isinstance(axis, LabelAxis):
597
label_axis.append(repr(axis))
599
positions.append(axis.positions_to_url())
601
styles.append(axis.style_to_url())
602
if not available_axis:
605
url_bits.append('chxt=%s' % ','.join(available_axis))
607
url_bits.append('chxl=%s' % '|'.join(label_axis))
609
url_bits.append('chxr=%s' % '|'.join(range_axis))
611
url_bits.append('chxp=%s' % '|'.join(positions))
613
url_bits.append('chxs=%s' % '|'.join(styles))
614
return '&'.join(url_bits)
616
# Markers, Ranges and Fill area (chm)
617
# -------------------------------------------------------------------------
619
def markers_to_url(self):
620
return 'chm=%s' % '|'.join([','.join(a) for a in self.markers])
622
def add_marker(self, index, point, marker_type, colour, size, priority=0):
623
self.markers.append((marker_type, colour, str(index), str(point), \
624
str(size), str(priority)))
626
def add_horizontal_range(self, colour, start, stop):
627
self.markers.append(('r', colour, '1', str(start), str(stop)))
629
def add_vertical_range(self, colour, start, stop):
630
self.markers.append(('R', colour, '1', str(start), str(stop)))
632
def add_fill_range(self, colour, index_start, index_end):
633
self.markers.append(('b', colour, str(index_start), str(index_end), \
636
def add_fill_simple(self, colour):
637
self.markers.append(('B', colour, '1', '1', '1'))
640
# -------------------------------------------------------------------------
642
def set_line_style(self, index, thickness=1, line_segment=None, \
645
value.append(str(thickness))
647
value.append(str(line_segment))
648
value.append(str(blank_segment))
649
self.line_styles[index] = value
652
# -------------------------------------------------------------------------
654
def set_grid(self, x_step, y_step, line_segment=1, \
656
self.grid = '%s,%s,%s,%s' % (x_step, y_step, line_segment, \
660
class ScatterChart(Chart):
662
def type_to_url(self):
665
def annotated_data(self):
666
yield ('x', self.data[0])
667
yield ('y', self.data[1])
668
if len(self.data) > 2:
669
# The optional third dataset is relative sizing for point
671
yield ('marker-size', self.data[2])
674
class LineChart(Chart):
676
def __init__(self, *args, **kwargs):
677
assert(type(self) != LineChart) # This is an abstract class
678
Chart.__init__(self, *args, **kwargs)
681
class SimpleLineChart(LineChart):
683
def type_to_url(self):
686
def annotated_data(self):
687
# All datasets are y-axis data.
688
for dataset in self.data:
692
class SparkLineChart(SimpleLineChart):
694
def type_to_url(self):
698
class XYLineChart(LineChart):
700
def type_to_url(self):
703
def annotated_data(self):
704
# Datasets alternate between x-axis, y-axis.
705
for i, dataset in enumerate(self.data):
712
class BarChart(Chart):
714
def __init__(self, *args, **kwargs):
715
assert(type(self) != BarChart) # This is an abstract class
716
Chart.__init__(self, *args, **kwargs)
717
self.bar_width = None
720
def set_bar_width(self, bar_width):
721
self.bar_width = bar_width
723
def set_zero_line(self, index, zero_line):
724
self.zero_lines[index] = zero_line
726
def get_url_bits(self, data_class=None, skip_chbh=False):
727
url_bits = Chart.get_url_bits(self, data_class=data_class)
728
if not skip_chbh and self.bar_width is not None:
729
url_bits.append('chbh=%i' % self.bar_width)
732
for index in xrange(max(self.zero_lines) + 1):
733
if index in self.zero_lines:
734
zero_line.append(str(self.zero_lines[index]))
736
zero_line.append('0')
737
url_bits.append('chp=%s' % ','.join(zero_line))
741
class StackedHorizontalBarChart(BarChart):
743
def type_to_url(self):
747
class StackedVerticalBarChart(BarChart):
749
def type_to_url(self):
752
def annotated_data(self):
753
for dataset in self.data:
757
class GroupedBarChart(BarChart):
759
def __init__(self, *args, **kwargs):
760
assert(type(self) != GroupedBarChart) # This is an abstract class
761
BarChart.__init__(self, *args, **kwargs)
762
self.bar_spacing = None
763
self.group_spacing = None
765
def set_bar_spacing(self, spacing):
766
"""Set spacing between bars in a group."""
767
self.bar_spacing = spacing
769
def set_group_spacing(self, spacing):
770
"""Set spacing between groups of bars."""
771
self.group_spacing = spacing
773
def get_url_bits(self, data_class=None):
774
# Skip 'BarChart.get_url_bits' and call Chart directly so the parent
775
# doesn't add "chbh" before we do.
776
url_bits = BarChart.get_url_bits(self, data_class=data_class,
778
if self.group_spacing is not None:
779
if self.bar_spacing is None:
780
raise InvalidParametersException('Bar spacing is required ' \
781
'to be set when setting group spacing')
782
if self.bar_width is None:
783
raise InvalidParametersException('Bar width is required to ' \
784
'be set when setting bar spacing')
785
url_bits.append('chbh=%i,%i,%i'
786
% (self.bar_width, self.bar_spacing, self.group_spacing))
787
elif self.bar_spacing is not None:
788
if self.bar_width is None:
789
raise InvalidParametersException('Bar width is required to ' \
790
'be set when setting bar spacing')
791
url_bits.append('chbh=%i,%i' % (self.bar_width, self.bar_spacing))
793
url_bits.append('chbh=%i' % self.bar_width)
797
class GroupedHorizontalBarChart(GroupedBarChart):
799
def type_to_url(self):
803
class GroupedVerticalBarChart(GroupedBarChart):
805
def type_to_url(self):
808
def annotated_data(self):
809
for dataset in self.data:
813
class PieChart(Chart):
815
def __init__(self, *args, **kwargs):
816
assert(type(self) != PieChart) # This is an abstract class
817
Chart.__init__(self, *args, **kwargs)
820
def set_pie_labels(self, labels):
821
self.pie_labels = [urllib.quote(a) for a in labels]
823
def get_url_bits(self, data_class=None):
824
url_bits = Chart.get_url_bits(self, data_class=data_class)
826
url_bits.append('chl=%s' % '|'.join(self.pie_labels))
829
def annotated_data(self):
830
# Datasets are all y-axis data. However, there should only be
831
# one dataset for pie charts.
832
for dataset in self.data:
836
class PieChart2D(PieChart):
838
def type_to_url(self):
842
class PieChart3D(PieChart):
844
def type_to_url(self):
848
class VennChart(Chart):
850
def type_to_url(self):
853
def annotated_data(self):
854
for dataset in self.data:
858
class RadarChart(Chart):
860
def type_to_url(self):
864
class SplineRadarChart(RadarChart):
866
def type_to_url(self):
870
class MapChart(Chart):
872
def __init__(self, *args, **kwargs):
873
Chart.__init__(self, *args, **kwargs)
874
self.geo_area = 'world'
877
def type_to_url(self):
880
def set_codes(self, codes):
883
def get_url_bits(self, data_class=None):
884
url_bits = Chart.get_url_bits(self, data_class=data_class)
885
url_bits.append('chtm=%s' % self.geo_area)
887
url_bits.append('chld=%s' % ''.join(self.codes))
891
class GoogleOMeterChart(PieChart):
892
"""Inheriting from PieChart because of similar labeling"""
894
def type_to_url(self):
899
chart = PieChart2D(320, 200)
900
chart = ScatterChart(320, 200)
901
chart = SimpleLineChart(320, 200)
902
chart = GroupedVerticalBarChart(320, 200)
903
# chart = SplineRadarChart(500, 500)
904
# chart = MapChart(440, 220)
905
# chart = GoogleOMeterChart(440, 220, x_range=(0, 100))
906
sine_data = [math.sin(float(a) / math.pi) * 100 for a in xrange(100)]
907
random_data = [random.random() * 100 for a in xrange(100)]
908
random_data2 = [random.random() * 50 for a in xrange(100)]
909
# chart.set_bar_width(50)
910
# chart.set_bar_spacing(0)
911
chart.add_data(sine_data)
912
chart.add_data(random_data)
913
# chart.set_zero_line(1, .5)
914
# chart.add_data(random_data2)
915
# chart.set_line_style(0, thickness=5)
916
# chart.set_line_style(1, thickness=2, line_segment=10, blank_segment=5)
917
# chart.set_title('heloooo weeee')
918
# chart.set_legend(('sine wave', 'random * x'))
919
chart.set_colours(('ee2000', 'DDDDAA', 'fF03f2'))
920
# chart.fill_solid(Chart.ALPHA, '123456')
921
# chart.fill_linear_gradient(Chart.ALPHA, 20, '004070', 1, '300040', 0,
923
# chart.fill_linear_stripes(Chart.CHART, 20, '204070', .2, '300040', .2,
925
# axis_left_index = chart.set_axis_range(Axis.LEFT, 0, 10)
926
# axis_right_index = chart.set_axis_range(Axis.RIGHT, 5, 30)
927
# axis_bottom_index = chart.set_axis_labels(Axis.BOTTOM, [1, 25, 95])
928
# chart.set_axis_positions(axis_bottom_index, [1, 25, 95])
929
# chart.set_axis_style(axis_bottom_index, '003050', 15)
931
# chart.set_pie_labels(('apples', 'oranges', 'bananas'))
933
# chart.set_grid(10, 10)
934
# for a in xrange(0, 100, 10):
935
# chart.add_marker(1, a, 'a', 'AACA20', 10)
937
# chart.add_horizontal_range('00A020', .2, .5)
938
# chart.add_vertical_range('00c030', .2, .4)
940
# chart.add_fill_simple('303030A0')
942
# chart.set_codes(['AU', 'AT', 'US'])
943
# chart.add_data([1,2,3])
944
# chart.set_colours(('EEEEEE', '000000', '00FF00'))
946
# chart.add_data([50,75])
947
# chart.set_pie_labels(('apples', 'oranges'))
949
url = chart.get_url()
952
chart.download('test.png')
955
data = urllib.urlopen(chart.get_url()).read()
956
open('meh.png', 'wb').write(data)
957
os.system('eog meh.png')
960
if __name__ == '__main__':