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
def float_scale_value(cls, value, range):
85
max_value = cls.max_value()
86
scaled = (value-lower) * (float(max_value)/(upper-lower))
90
def clip_value(cls, value):
91
clipped = max(0, min(value, cls.max_value()))
95
def int_scale_value(cls, value, range):
96
scaled = int(round(cls.float_scale_value(value, range)))
100
def scale_value(cls, value, range):
101
scaled = cls.int_scale_value(value, range)
102
clipped = cls.clip_value(scaled)
105
class SimpleData(Data):
106
enc_map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
109
max_value = self.max_value()
111
for data in self.data:
116
elif value >= 0 and value <= max_value:
117
sub_data.append(SimpleData.enc_map[value])
119
raise DataOutOfRangeException('cannot encode value: %d'
121
encoded_data.append(''.join(sub_data))
122
return 'chd=s:' + ','.join(encoded_data)
128
class TextData(Data):
131
max_value = self.max_value()
133
for data in self.data:
138
elif value >= 0 and value <= max_value:
139
sub_data.append("%.1f" % float(value))
141
raise DataOutOfRangeException()
142
encoded_data.append(','.join(sub_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))
161
def scale_value(cls, value, range):
162
# use float values instead of integers because we don't need an encode
164
scaled = cls.float_scale_value(value,range)
165
clipped = cls.clip_value(scaled)
168
class ExtendedData(Data):
170
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-.'
173
max_value = self.max_value()
175
enc_size = len(ExtendedData.enc_map)
176
for data in self.data:
180
sub_data.append('__')
181
elif value >= 0 and value <= max_value:
182
first, second = divmod(int(value), enc_size)
183
sub_data.append('%s%s' % (
184
ExtendedData.enc_map[first],
185
ExtendedData.enc_map[second]))
187
raise DataOutOfRangeException( \
188
'Item #%i "%s" is out of range' % (data.index(value), \
190
encoded_data.append(''.join(sub_data))
191
return 'chd=e:' + ','.join(encoded_data)
199
# -----------------------------------------------------------------------------
207
TYPES = (BOTTOM, TOP, LEFT, RIGHT)
209
def __init__(self, axis_index, axis_type, **kw):
210
assert(axis_type in Axis.TYPES)
211
self.has_style = False
212
self.axis_index = axis_index
213
self.axis_type = axis_type
214
self.positions = None
216
def set_index(self, axis_index):
217
self.axis_index = axis_index
219
def set_positions(self, positions):
220
self.positions = positions
222
def set_style(self, colour, font_size=None, alignment=None):
223
_check_colour(colour)
225
self.font_size = font_size
226
self.alignment = alignment
227
self.has_style = True
229
def style_to_url(self):
231
bits.append(str(self.axis_index))
232
bits.append(self.colour)
233
if self.font_size is not None:
234
bits.append(str(self.font_size))
235
if self.alignment is not None:
236
bits.append(str(self.alignment))
237
return ','.join(bits)
239
def positions_to_url(self):
241
bits.append(str(self.axis_index))
242
bits += [str(a) for a in self.positions]
243
return ','.join(bits)
246
class LabelAxis(Axis):
248
def __init__(self, axis_index, axis_type, values, **kwargs):
249
Axis.__init__(self, axis_index, axis_type, **kwargs)
250
self.values = [str(a) for a in values]
253
return '%i:|%s' % (self.axis_index, '|'.join(self.values))
256
class RangeAxis(Axis):
258
def __init__(self, axis_index, axis_type, low, high, **kwargs):
259
Axis.__init__(self, axis_index, axis_type, **kwargs)
264
return '%i,%s,%s' % (self.axis_index, self.low, self.high)
267
# -----------------------------------------------------------------------------
271
"""Abstract class for all chart types.
273
width are height specify the dimensions of the image. title sets the title
274
of the chart. legend requires a list that corresponds to datasets.
277
BASE_URL = 'http://chart.apis.google.com/chart?'
281
VALID_SOLID_FILL_TYPES = (BACKGROUND, CHART, ALPHA)
283
LINEAR_GRADIENT = 'lg'
284
LINEAR_STRIPES = 'ls'
286
def __init__(self, width, height, title=None, legend=None, colours=None,
287
auto_scale=True, x_range=None, y_range=None):
288
assert(type(self) != Chart) # This is an abstract class
289
assert(isinstance(width, int))
290
assert(isinstance(height, int))
294
self.set_title(title)
295
self.set_legend(legend)
296
self.set_colours(colours)
299
self.auto_scale = auto_scale # Whether to automatically scale data
300
self.x_range = x_range # (min, max) x-axis range for scaling
301
self.y_range = y_range # (min, max) y-axis range for scaling
302
self.scaled_data_class = None
303
self.scaled_x_range = None
304
self.scaled_y_range = None
307
Chart.BACKGROUND: None,
312
Chart.BACKGROUND: None,
318
self.line_styles = {}
322
# -------------------------------------------------------------------------
324
def get_url(self, data_class=None):
325
url_bits = self.get_url_bits(data_class=data_class)
326
return self.BASE_URL + '&'.join(url_bits)
328
def get_url_bits(self, data_class=None):
331
url_bits.append(self.type_to_url())
332
url_bits.append('chs=%ix%i' % (self.width, self.height))
333
url_bits.append(self.data_to_url(data_class=data_class))
336
url_bits.append('chtt=%s' % self.title)
338
url_bits.append('chdl=%s' % '|'.join(self.legend))
340
url_bits.append('chco=%s' % ','.join(self.colours))
341
ret = self.fill_to_url()
344
ret = self.axis_to_url()
348
url_bits.append(self.markers_to_url())
351
for index in xrange(max(self.line_styles) + 1):
352
if index in self.line_styles:
353
values = self.line_styles[index]
356
style.append(','.join(values))
357
url_bits.append('chls=%s' % '|'.join(style))
359
url_bits.append('chg=%s' % self.grid)
363
# -------------------------------------------------------------------------
365
def download(self, file_name):
366
opener = urllib2.urlopen(self.get_url())
368
if opener.headers['content-type'] != 'image/png':
369
raise BadContentTypeException('Server responded with a ' \
370
'content-type of %s' % opener.headers['content-type'])
372
open(file_name, 'wb').write(urllib.urlopen(self.get_url()).read())
375
# -------------------------------------------------------------------------
377
def set_title(self, title):
379
self.title = urllib.quote(title)
383
def set_legend(self, legend):
384
"""legend needs to be a list, tuple or None"""
385
assert(isinstance(legend, list) or isinstance(legend, tuple) or
388
self.legend = [urllib.quote(a) for a in legend]
393
# -------------------------------------------------------------------------
395
def set_colours(self, colours):
396
# colours needs to be a list, tuple or None
397
assert(isinstance(colours, list) or isinstance(colours, tuple) or
399
# make sure the colours are in the right format
403
self.colours = colours
405
# Background/Chart colours
406
# -------------------------------------------------------------------------
408
def fill_solid(self, area, colour):
409
assert(area in Chart.VALID_SOLID_FILL_TYPES)
410
_check_colour(colour)
411
self.fill_area[area] = colour
412
self.fill_types[area] = Chart.SOLID
414
def _check_fill_linear(self, angle, *args):
415
assert(isinstance(args, list) or isinstance(args, tuple))
416
assert(angle >= 0 and angle <= 90)
417
assert(len(args) % 2 == 0)
418
args = list(args) # args is probably a tuple and we need to mutate
419
for a in xrange(len(args) / 2):
421
offset = args[a * 2 + 1]
423
assert(offset >= 0 and offset <= 1)
424
args[a * 2 + 1] = str(args[a * 2 + 1])
427
def fill_linear_gradient(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_GRADIENT
431
self.fill_area[area] = ','.join([str(angle)] + args)
433
def fill_linear_stripes(self, area, angle, *args):
434
assert(area in Chart.VALID_SOLID_FILL_TYPES)
435
args = self._check_fill_linear(angle, *args)
436
self.fill_types[area] = Chart.LINEAR_STRIPES
437
self.fill_area[area] = ','.join([str(angle)] + args)
439
def fill_to_url(self):
441
for area in (Chart.BACKGROUND, Chart.CHART, Chart.ALPHA):
442
if self.fill_types[area]:
443
areas.append('%s,%s,%s' % (area, self.fill_types[area], \
444
self.fill_area[area]))
446
return 'chf=' + '|'.join(areas)
449
# -------------------------------------------------------------------------
451
def data_class_detection(self, data):
452
"""Determines the appropriate data encoding type to give satisfactory
453
resolution (http://code.google.com/apis/chart/#chart_data).
455
assert(isinstance(data, list) or isinstance(data, tuple))
456
if not isinstance(self, (LineChart, BarChart, ScatterChart)):
457
# From the link above:
458
# Simple encoding is suitable for all other types of chart
459
# regardless of size.
461
elif self.height < 100:
462
# The link above indicates that line and bar charts less
463
# than 300px in size can be suitably represented with the
464
# simple encoding. I've found that this isn't sufficient,
465
# e.g. examples/line-xy-circle.png. Let's try 100px.
470
def data_x_range(self):
471
"""Return a 2-tuple giving the minimum and maximum x-axis
475
lower = min([min(s) for type, s in self.annotated_data()
477
upper = max([max(s) for type, s in self.annotated_data()
479
return (lower, upper)
481
return None # no x-axis datasets
483
def data_y_range(self):
484
"""Return a 2-tuple giving the minimum and maximum y-axis
488
lower = min([min(s) for type, s in self.annotated_data()
490
upper = max([max(s) for type, s in self.annotated_data()
492
return (lower, upper)
494
return None # no y-axis datasets
496
def scaled_data(self, data_class, x_range=None, y_range=None):
497
"""Scale `self.data` as appropriate for the given data encoding
498
(data_class) and return it.
500
An optional `y_range` -- a 2-tuple (lower, upper) -- can be
501
given to specify the y-axis bounds. If not given, the range is
502
inferred from the data: (0, <max-value>) presuming no negative
503
values, or (<min-value>, <max-value>) if there are negative
504
values. `self.scaled_y_range` is set to the actual lower and
507
Ditto for `x_range`. Note that some chart types don't have x-axis
510
self.scaled_data_class = data_class
512
# Determine the x-axis range for scaling.
514
x_range = self.data_x_range()
515
if x_range and x_range[0] > 0:
516
x_range = (0, x_range[1])
517
self.scaled_x_range = x_range
519
# Determine the y-axis range for scaling.
521
y_range = self.data_y_range()
522
if y_range and y_range[0] > 0:
523
y_range = (0, y_range[1])
524
self.scaled_y_range = y_range
527
for type, dataset in self.annotated_data():
529
scale_range = x_range
531
scale_range = y_range
532
elif type == 'marker-size':
533
scale_range = (0, max(dataset))
534
scaled_data.append([data_class.scale_value(v, scale_range)
538
def add_data(self, data):
539
self.data.append(data)
540
return len(self.data) - 1 # return the "index" of the data set
542
def data_to_url(self, data_class=None):
544
data_class = self.data_class_detection(self.data)
545
if not issubclass(data_class, Data):
546
raise UnknownDataTypeException()
548
data = self.scaled_data(data_class, self.x_range, self.y_range)
551
return repr(data_class(data))
553
def annotated_data(self):
554
for dataset in self.data:
558
# -------------------------------------------------------------------------
560
def set_axis_labels(self, axis_type, values):
561
assert(axis_type in Axis.TYPES)
562
values = [ urllib.quote(a) for a in values ]
563
axis_index = len(self.axis)
564
axis = LabelAxis(axis_index, axis_type, values)
565
self.axis.append(axis)
568
def set_axis_range(self, axis_type, low, high):
569
assert(axis_type in Axis.TYPES)
570
axis_index = len(self.axis)
571
axis = RangeAxis(axis_index, axis_type, low, high)
572
self.axis.append(axis)
575
def set_axis_positions(self, axis_index, positions):
577
self.axis[axis_index].set_positions(positions)
579
raise InvalidParametersException('Axis index %i has not been ' \
582
def set_axis_style(self, axis_index, colour, font_size=None, \
585
self.axis[axis_index].set_style(colour, font_size, alignment)
587
raise InvalidParametersException('Axis index %i has not been ' \
590
def axis_to_url(self):
597
for axis in self.axis:
598
available_axis.append(axis.axis_type)
599
if isinstance(axis, RangeAxis):
600
range_axis.append(repr(axis))
601
if isinstance(axis, LabelAxis):
602
label_axis.append(repr(axis))
604
positions.append(axis.positions_to_url())
606
styles.append(axis.style_to_url())
607
if not available_axis:
610
url_bits.append('chxt=%s' % ','.join(available_axis))
612
url_bits.append('chxl=%s' % '|'.join(label_axis))
614
url_bits.append('chxr=%s' % '|'.join(range_axis))
616
url_bits.append('chxp=%s' % '|'.join(positions))
618
url_bits.append('chxs=%s' % '|'.join(styles))
619
return '&'.join(url_bits)
621
# Markers, Ranges and Fill area (chm)
622
# -------------------------------------------------------------------------
624
def markers_to_url(self):
625
return 'chm=%s' % '|'.join([','.join(a) for a in self.markers])
627
def add_marker(self, index, point, marker_type, colour, size, priority=0):
628
self.markers.append((marker_type, colour, str(index), str(point), \
629
str(size), str(priority)))
631
def add_horizontal_range(self, colour, start, stop):
632
self.markers.append(('r', colour, '1', str(start), str(stop)))
634
def add_vertical_range(self, colour, start, stop):
635
self.markers.append(('R', colour, '1', str(start), str(stop)))
637
def add_fill_range(self, colour, index_start, index_end):
638
self.markers.append(('b', colour, str(index_start), str(index_end), \
641
def add_fill_simple(self, colour):
642
self.markers.append(('B', colour, '1', '1', '1'))
645
# -------------------------------------------------------------------------
647
def set_line_style(self, index, thickness=1, line_segment=None, \
650
value.append(str(thickness))
652
value.append(str(line_segment))
653
value.append(str(blank_segment))
654
self.line_styles[index] = value
657
# -------------------------------------------------------------------------
659
def set_grid(self, x_step, y_step, line_segment=1, \
661
self.grid = '%s,%s,%s,%s' % (x_step, y_step, line_segment, \
665
class ScatterChart(Chart):
667
def type_to_url(self):
670
def annotated_data(self):
671
yield ('x', self.data[0])
672
yield ('y', self.data[1])
673
if len(self.data) > 2:
674
# The optional third dataset is relative sizing for point
676
yield ('marker-size', self.data[2])
678
class LineChart(Chart):
680
def __init__(self, *args, **kwargs):
681
assert(type(self) != LineChart) # This is an abstract class
682
Chart.__init__(self, *args, **kwargs)
684
# def get_url_bits(self, data_class=None):
685
# url_bits = Chart.get_url_bits(self, data_class=data_class)
689
class SimpleLineChart(LineChart):
691
def type_to_url(self):
694
def annotated_data(self):
695
# All datasets are y-axis data.
696
for dataset in self.data:
699
class SparkLineChart(SimpleLineChart):
701
def type_to_url(self):
704
class XYLineChart(LineChart):
706
def type_to_url(self):
709
def annotated_data(self):
710
# Datasets alternate between x-axis, y-axis.
711
for i, dataset in enumerate(self.data):
717
class BarChart(Chart):
719
def __init__(self, *args, **kwargs):
720
assert(type(self) != BarChart) # This is an abstract class
721
Chart.__init__(self, *args, **kwargs)
722
self.bar_width = None
725
def set_bar_width(self, bar_width):
726
self.bar_width = bar_width
728
def set_zero_line(self, index, zero_line):
729
self.zero_lines[index] = zero_line
731
def get_url_bits(self, data_class=None, skip_chbh=False):
732
url_bits = Chart.get_url_bits(self, data_class=data_class)
733
if not skip_chbh and self.bar_width is not None:
734
url_bits.append('chbh=%i' % self.bar_width)
737
for index in xrange(max(self.zero_lines) + 1):
738
if index in self.zero_lines:
739
zero_line.append(str(self.zero_lines[index]))
741
zero_line.append('0')
742
url_bits.append('chp=%s' % ','.join(zero_line))
746
class StackedHorizontalBarChart(BarChart):
748
def type_to_url(self):
752
class StackedVerticalBarChart(BarChart):
754
def type_to_url(self):
757
def annotated_data(self):
758
for dataset in self.data:
762
class GroupedBarChart(BarChart):
764
def __init__(self, *args, **kwargs):
765
assert(type(self) != GroupedBarChart) # This is an abstract class
766
BarChart.__init__(self, *args, **kwargs)
767
self.bar_spacing = None
768
self.group_spacing = None
770
def set_bar_spacing(self, spacing):
771
"""Set spacing between bars in a group."""
772
self.bar_spacing = spacing
774
def set_group_spacing(self, spacing):
775
"""Set spacing between groups of bars."""
776
self.group_spacing = spacing
778
def get_url_bits(self, data_class=None):
779
# Skip 'BarChart.get_url_bits' and call Chart directly so the parent
780
# doesn't add "chbh" before we do.
781
url_bits = BarChart.get_url_bits(self, data_class=data_class,
783
if self.group_spacing is not None:
784
if self.bar_spacing is None:
785
raise InvalidParametersException('Bar spacing is required to ' \
786
'be set when setting group spacing')
787
if self.bar_width is None:
788
raise InvalidParametersException('Bar width is required to ' \
789
'be set when setting bar spacing')
790
url_bits.append('chbh=%i,%i,%i'
791
% (self.bar_width, self.bar_spacing, self.group_spacing))
792
elif self.bar_spacing is not None:
793
if self.bar_width is None:
794
raise InvalidParametersException('Bar width is required to ' \
795
'be set when setting bar spacing')
796
url_bits.append('chbh=%i,%i' % (self.bar_width, self.bar_spacing))
798
url_bits.append('chbh=%i' % self.bar_width)
802
class GroupedHorizontalBarChart(GroupedBarChart):
804
def type_to_url(self):
808
class GroupedVerticalBarChart(GroupedBarChart):
810
def type_to_url(self):
813
def annotated_data(self):
814
for dataset in self.data:
818
class PieChart(Chart):
820
def __init__(self, *args, **kwargs):
821
assert(type(self) != PieChart) # This is an abstract class
822
Chart.__init__(self, *args, **kwargs)
825
def set_pie_labels(self, labels):
826
self.pie_labels = [urllib.quote(a) for a in labels]
828
def get_url_bits(self, data_class=None):
829
url_bits = Chart.get_url_bits(self, data_class=data_class)
831
url_bits.append('chl=%s' % '|'.join(self.pie_labels))
834
def annotated_data(self):
835
# Datasets are all y-axis data. However, there should only be
836
# one dataset for pie charts.
837
for dataset in self.data:
841
class PieChart2D(PieChart):
843
def type_to_url(self):
847
class PieChart3D(PieChart):
849
def type_to_url(self):
853
class VennChart(Chart):
855
def type_to_url(self):
858
def annotated_data(self):
859
for dataset in self.data:
863
class RadarChart(Chart):
865
def type_to_url(self):
868
class SplineRadarChart(RadarChart):
870
def type_to_url(self):
874
class MapChart(Chart):
876
def __init__(self, *args, **kwargs):
877
Chart.__init__(self, *args, **kwargs)
878
self.geo_area = 'world'
881
def type_to_url(self):
884
def set_codes(self, codes):
887
def get_url_bits(self, data_class=None):
888
url_bits = Chart.get_url_bits(self, data_class=data_class)
889
url_bits.append('chtm=%s' % self.geo_area)
891
url_bits.append('chld=%s' % ''.join(self.codes))
895
class GoogleOMeterChart(PieChart):
896
"""Inheriting from PieChart because of similar labeling"""
898
def type_to_url(self):
903
chart = PieChart2D(320, 200)
904
chart = ScatterChart(320, 200)
905
chart = SimpleLineChart(320, 200)
906
chart = GroupedVerticalBarChart(320, 200)
907
# chart = SplineRadarChart(500, 500)
908
# chart = MapChart(440, 220)
909
# chart = GoogleOMeterChart(440, 220, x_range=(0, 100))
910
sine_data = [math.sin(float(a) / math.pi) * 100 for a in xrange(100)]
911
random_data = [random.random() * 100 for a in xrange(100)]
912
random_data2 = [random.random() * 50 for a in xrange(100)]
913
# chart.set_bar_width(50)
914
# chart.set_bar_spacing(0)
915
chart.add_data(sine_data)
916
chart.add_data(random_data)
917
chart.set_zero_line(1, .5)
918
# chart.add_data(random_data2)
919
# chart.set_line_style(0, thickness=5)
920
# chart.set_line_style(1, thickness=2, line_segment=10, blank_segment=5)
921
# chart.set_title('heloooo weeee')
922
# chart.set_legend(('sine wave', 'random * x'))
923
chart.set_colours(('ee2000', 'DDDDAA', 'fF03f2'))
924
# chart.fill_solid(Chart.ALPHA, '123456')
925
# chart.fill_linear_gradient(Chart.ALPHA, 20, '004070', 1, '300040', 0,
927
# chart.fill_linear_stripes(Chart.CHART, 20, '204070', .2, '300040', .2,
929
# axis_left_index = chart.set_axis_range(Axis.LEFT, 0, 10)
930
# axis_right_index = chart.set_axis_range(Axis.RIGHT, 5, 30)
931
# axis_bottom_index = chart.set_axis_labels(Axis.BOTTOM, [1, 25, 95])
932
# chart.set_axis_positions(axis_bottom_index, [1, 25, 95])
933
# chart.set_axis_style(axis_bottom_index, '003050', 15)
935
# chart.set_pie_labels(('apples', 'oranges', 'bananas'))
937
# chart.set_grid(10, 10)
938
# for a in xrange(0, 100, 10):
939
# chart.add_marker(1, a, 'a', 'AACA20', 10)
941
# chart.add_horizontal_range('00A020', .2, .5)
942
# chart.add_vertical_range('00c030', .2, .4)
944
# chart.add_fill_simple('303030A0')
946
# chart.set_codes(['AU', 'AT', 'US'])
947
# chart.add_data([1,2,3])
948
# chart.set_colours(('EEEEEE', '000000', '00FF00'))
950
# chart.add_data([50,75])
951
# chart.set_pie_labels(('apples', 'oranges'))
953
url = chart.get_url()
956
chart.download('test.png')
959
data = urllib.urlopen(chart.get_url()).read()
960
open('meh.png', 'wb').write(data)
961
os.system('eog meh.png')
964
if __name__ == '__main__':
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
# -----------------------------------------------------------------------------
33
reo_colour = re.compile('^([A-Fa-f0-9]{2,2}){3,4}$')
36
def _check_colour(colour):
37
if not reo_colour.match(colour):
38
raise InvalidParametersException('Colours need to be in ' \
39
'RRGGBB or RRGGBBAA format. One of your colours has %s' % \
43
# -----------------------------------------------------------------------------
46
class PyGoogleChartException(Exception):
50
class DataOutOfRangeException(PyGoogleChartException):
54
class UnknownDataTypeException(PyGoogleChartException):
58
class NoDataGivenException(PyGoogleChartException):
62
class InvalidParametersException(PyGoogleChartException):
66
class BadContentTypeException(PyGoogleChartException):
71
# -----------------------------------------------------------------------------
76
def __init__(self, data):
77
assert(type(self) != Data) # This is an abstract class
81
class SimpleData(Data):
82
enc_map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
86
for data in self.data:
91
elif value >= 0 and value <= SimpleData.max_value:
92
sub_data.append(SimpleData.enc_map[value])
94
raise DataOutOfRangeException()
95
encoded_data.append(''.join(sub_data))
96
return 'chd=s:' + ','.join(encoded_data)
103
class TextData(Data):
107
for data in self.data:
112
elif value >= 0 and value <= TextData.max_value:
113
sub_data.append(str(float(value)))
115
raise DataOutOfRangeException()
116
encoded_data.append(','.join(sub_data))
117
return 'chd=t:' + '|'.join(encoded_data)
124
class ExtendedData(Data):
126
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-.'
130
enc_size = len(ExtendedData.enc_map)
131
for data in self.data:
135
sub_data.append('__')
136
elif value >= 0 and value <= ExtendedData.max_value:
137
first, second = divmod(int(value), enc_size)
138
sub_data.append('%s%s' % (
139
ExtendedData.enc_map[first],
140
ExtendedData.enc_map[second]))
142
raise DataOutOfRangeException( \
143
'Item #%i "%s" is out of range' % (data.index(value), \
145
encoded_data.append(''.join(sub_data))
146
return 'chd=e:' + ','.join(encoded_data)
153
# -----------------------------------------------------------------------------
161
TYPES = (BOTTOM, TOP, LEFT, RIGHT)
163
def __init__(self, axis, **kw):
164
assert(axis in Axis.TYPES)
165
self.has_style = False
167
self.positions = None
169
def set_index(self, index):
172
def set_positions(self, positions):
173
self.positions = positions
175
def set_style(self, colour, font_size=None, alignment=None):
176
_check_colour(colour)
178
self.font_size = font_size
179
self.alignment = alignment
180
self.has_style = True
182
def style_to_url(self):
184
bits.append(str(self.index))
185
bits.append(self.colour)
186
if self.font_size is not None:
187
bits.append(str(self.font_size))
188
if self.alignment is not None:
189
bits.append(str(self.alignment))
190
return ','.join(bits)
192
def positions_to_url(self):
194
bits.append(str(self.index))
195
bits += [str(a) for a in self.positions]
196
return ','.join(bits)
199
class LabelAxis(Axis):
201
def __init__(self, axis, values, **kwargs):
202
Axis.__init__(self, axis, **kwargs)
203
self.values = [str(a) for a in values]
206
return '%i:|%s' % (self.index, '|'.join(self.values))
209
class RangeAxis(Axis):
211
def __init__(self, axis, low, high, **kwargs):
212
Axis.__init__(self, axis, **kwargs)
217
return '%i,%s,%s' % (self.index, self.low, self.high)
220
# -----------------------------------------------------------------------------
224
"""Abstract class for all chart types.
226
width are height specify the dimensions of the image. title sets the title
227
of the chart. legend requires a list that corresponds to datasets.
230
BASE_URL = 'http://chart.apis.google.com/chart?'
234
LINEAR_GRADIENT = 'lg'
235
LINEAR_STRIPES = 'ls'
237
def __init__(self, width, height, title=None, legend=None, colours=None):
238
assert(type(self) != Chart) # This is an abstract class
239
assert(isinstance(width, int))
240
assert(isinstance(height, int))
244
self.set_title(title)
245
self.set_legend(legend)
246
self.set_colours(colours)
248
Chart.BACKGROUND: None,
252
Chart.BACKGROUND: None,
264
# -------------------------------------------------------------------------
267
url_bits = self.get_url_bits()
268
return self.BASE_URL + '&'.join(url_bits)
270
def get_url_bits(self):
273
url_bits.append(self.type_to_url())
274
url_bits.append('chs=%ix%i' % (self.width, self.height))
275
url_bits.append(self.data_to_url())
278
url_bits.append('chtt=%s' % self.title)
280
url_bits.append('chdl=%s' % '|'.join(self.legend))
282
url_bits.append('chco=%s' % ','.join(self.colours))
283
ret = self.fill_to_url()
286
ret = self.axis_to_url()
290
url_bits.append(self.markers_to_url())
294
# -------------------------------------------------------------------------
296
def download(self, file_name):
297
opener = urllib2.urlopen(self.get_url())
299
if opener.headers['content-type'] != 'image/png':
300
raise BadContentTypeException('Server responded with a ' \
301
'content-type of %s' % opener.headers['content-type'])
303
open(file_name, 'wb').write(urllib.urlopen(self.get_url()).read())
306
# -------------------------------------------------------------------------
308
def set_title(self, title):
310
self.title = urllib.quote(title)
314
def set_legend(self, legend):
315
# legend needs to be a list, tuple or None
316
assert(isinstance(legend, list) or isinstance(legend, tuple) or
319
self.legend = [urllib.quote(a) for a in legend]
324
# -------------------------------------------------------------------------
326
def set_colours(self, colours):
327
# colours needs to be a list, tuple or None
328
assert(isinstance(colours, list) or isinstance(colours, tuple) or
330
# make sure the colours are in the right format
334
self.colours = colours
336
# Background/Chart colours
337
# -------------------------------------------------------------------------
339
def fill_solid(self, area, colour):
340
assert(area in (Chart.BACKGROUND, Chart.CHART))
341
_check_colour(colour)
342
self.fill_area[area] = colour
343
self.fill_types[area] = Chart.SOLID
345
def _check_fill_linear(self, angle, *args):
346
assert(isinstance(args, list) or isinstance(args, tuple))
347
assert(angle >= 0 and angle <= 90)
348
assert(len(args) % 2 == 0)
349
args = list(args) # args is probably a tuple and we need to mutate
350
for a in xrange(len(args) / 2):
352
offset = args[a * 2 + 1]
354
assert(offset >= 0 and offset <= 1)
355
args[a * 2 + 1] = str(args[a * 2 + 1])
358
def fill_linear_gradient(self, area, angle, *args):
359
assert(area in (Chart.BACKGROUND, Chart.CHART))
360
args = self._check_fill_linear(angle, *args)
361
self.fill_types[area] = Chart.LINEAR_GRADIENT
362
self.fill_area[area] = ','.join([str(angle)] + args)
364
def fill_linear_stripes(self, area, angle, *args):
365
assert(area in (Chart.BACKGROUND, Chart.CHART))
366
args = self._check_fill_linear(angle, *args)
367
self.fill_types[area] = Chart.LINEAR_STRIPES
368
self.fill_area[area] = ','.join([str(angle)] + args)
370
def fill_to_url(self):
372
for area in (Chart.BACKGROUND, Chart.CHART):
373
if self.fill_types[area]:
374
areas.append('%s,%s,%s' % (area, self.fill_types[area], \
375
self.fill_area[area]))
377
return 'chf=' + '|'.join(areas)
380
# -------------------------------------------------------------------------
382
def data_class_detection(self, data):
384
Detects and returns the data type required based on the range of the
385
data given. The data given must be lists of numbers within a list.
387
assert(isinstance(data, list) or isinstance(data, tuple))
390
assert(isinstance(a, list) or isinstance(a, tuple))
391
if max_value is None or max(a) > max_value:
393
for data_class in (SimpleData, TextData, ExtendedData):
394
if max_value <= data_class.max_value():
396
raise DataOutOfRangeException()
398
def add_data(self, data):
399
self.data.append(data)
400
return len(self.data) - 1 # return the "index" of the data set
402
def data_to_url(self, data_class=None):
404
data_class = self.data_class_detection(self.data)
405
if not issubclass(data_class, Data):
406
raise UnknownDataTypeException()
407
return repr(data_class(self.data))
410
# -------------------------------------------------------------------------
412
def set_axis_labels(self, axis, values):
413
assert(axis in Axis.TYPES)
414
self.axis[axis] = LabelAxis(axis, values)
416
def set_axis_range(self, axis, low, high):
417
assert(axis in Axis.TYPES)
418
self.axis[axis] = RangeAxis(axis, low, high)
420
def set_axis_positions(self, axis, positions):
421
assert(axis in Axis.TYPES)
422
if not self.axis[axis]:
423
raise InvalidParametersException('Please create an axis first')
424
self.axis[axis].set_positions(positions)
426
def set_axis_style(self, axis, colour, font_size=None, alignment=None):
427
assert(axis in Axis.TYPES)
428
if not self.axis[axis]:
429
raise InvalidParametersException('Please create an axis first')
430
self.axis[axis].set_style(colour, font_size, alignment)
432
def axis_to_url(self):
439
for position, axis in self.axis.items():
443
axis.set_index(index)
444
available_axis.append(position)
445
if isinstance(axis, RangeAxis):
446
range_axis.append(repr(axis))
447
if isinstance(axis, LabelAxis):
448
label_axis.append(repr(axis))
450
positions.append(axis.positions_to_url())
452
styles.append(axis.style_to_url())
453
if not available_axis:
456
url_bits.append('chxt=%s' % ','.join(available_axis))
458
url_bits.append('chxl=%s' % '|'.join(label_axis))
460
url_bits.append('chxr=%s' % '|'.join(range_axis))
462
url_bits.append('chxp=%s' % '|'.join(positions))
464
url_bits.append('chxs=%s' % '|'.join(styles))
465
return '&'.join(url_bits)
467
# Markers, Ranges and Fill area (chm)
468
# -------------------------------------------------------------------------
470
def markers_to_url(self):
471
return 'chm=%s' % '|'.join([','.join(a) for a in self.markers])
473
def add_marker(self, index, point, marker_type, colour, size):
474
self.markers.append((marker_type, colour, str(index), str(point), \
477
def add_horizontal_range(self, colour, start, stop):
478
self.markers.append(('r', colour, '1', str(start), str(stop)))
480
def add_vertical_range(self, colour, start, stop):
481
self.markers.append(('R', colour, '1', str(start), str(stop)))
483
def add_fill_range(self, colour, index_start, index_end):
484
self.markers.append(('b', colour, str(index_start), str(index_end), \
487
def add_fill_simple(self, colour):
488
self.markers.append(('B', colour, '1', '1', '1'))
491
class ScatterChart(Chart):
493
def __init__(self, *args, **kwargs):
494
Chart.__init__(self, *args, **kwargs)
496
def type_to_url(self):
500
class LineChart(Chart):
502
def __init__(self, *args, **kwargs):
503
assert(type(self) != LineChart) # This is an abstract class
504
Chart.__init__(self, *args, **kwargs)
505
self.line_styles = {}
508
def set_line_style(self, index, thickness=1, line_segment=None, \
511
value.append(str(thickness))
513
value.append(str(line_segment))
514
value.append(str(blank_segment))
515
self.line_styles[index] = value
517
def set_grid(self, x_step, y_step, line_segment=1, \
519
self.grid = '%s,%s,%s,%s' % (x_step, y_step, line_segment, \
522
def get_url_bits(self):
523
url_bits = Chart.get_url_bits(self)
526
# for index, values in self.line_style.items():
527
for index in xrange(max(self.line_styles) + 1):
528
if index in self.line_styles:
529
values = self.line_styles[index]
532
style.append(','.join(values))
533
url_bits.append('chls=%s' % '|'.join(style))
535
url_bits.append('chg=%s' % self.grid)
539
class SimpleLineChart(LineChart):
541
def type_to_url(self):
545
class XYLineChart(LineChart):
547
def type_to_url(self):
551
class BarChart(Chart):
553
def __init__(self, *args, **kwargs):
554
assert(type(self) != BarChart) # This is an abstract class
555
Chart.__init__(self, *args, **kwargs)
556
self.bar_width = None
558
def set_bar_width(self, bar_width):
559
self.bar_width = bar_width
561
def get_url_bits(self):
562
url_bits = Chart.get_url_bits(self)
563
url_bits.append('chbh=%i' % self.bar_width)
567
class StackedHorizontalBarChart(BarChart):
569
def type_to_url(self):
573
class StackedVerticalBarChart(BarChart):
575
def type_to_url(self):
579
class GroupedBarChart(BarChart):
581
def __init__(self, *args, **kwargs):
582
assert(type(self) != GroupedBarChart) # This is an abstract class
583
BarChart.__init__(self, *args, **kwargs)
584
self.bar_spacing = None
586
def set_bar_spacing(self, spacing):
587
self.bar_spacing = spacing
589
def get_url_bits(self):
590
# Skip 'BarChart.get_url_bits' and call Chart directly so the parent
591
# doesn't add "chbh" before we do.
592
url_bits = Chart.get_url_bits(self)
593
if self.bar_spacing is not None:
594
if self.bar_width is None:
595
raise InvalidParametersException('Bar width is required to ' \
596
'be set when setting spacing')
597
url_bits.append('chbh=%i,%i' % (self.bar_width, self.bar_spacing))
599
url_bits.append('chbh=%i' % self.bar_width)
603
class GroupedHorizontalBarChart(GroupedBarChart):
605
def type_to_url(self):
609
class GroupedVerticalBarChart(GroupedBarChart):
611
def type_to_url(self):
615
class PieChart(Chart):
617
def __init__(self, *args, **kwargs):
618
assert(type(self) != PieChart) # This is an abstract class
619
Chart.__init__(self, *args, **kwargs)
622
def set_pie_labels(self, labels):
623
self.pie_labels = labels
625
def get_url_bits(self):
626
url_bits = Chart.get_url_bits(self)
628
url_bits.append('chl=%s' % '|'.join(self.pie_labels))
632
class PieChart2D(PieChart):
634
def type_to_url(self):
638
class PieChart3D(PieChart):
640
def type_to_url(self):
644
class VennChart(Chart):
646
def type_to_url(self):
651
chart = GroupedVerticalBarChart(320, 200)
652
chart = PieChart2D(320, 200)
653
chart = ScatterChart(320, 200)
654
chart = SimpleLineChart(320, 200)
655
sine_data = [math.sin(float(a) / 10) * 2000 + 2000 for a in xrange(100)]
656
random_data = [a * random.random() * 30 for a in xrange(40)]
657
random_data2 = [random.random() * 4000 for a in xrange(10)]
658
# chart.set_bar_width(50)
659
# chart.set_bar_spacing(0)
660
chart.add_data(sine_data)
661
chart.add_data(random_data)
662
chart.add_data(random_data2)
663
# chart.set_line_style(1, thickness=2)
664
# chart.set_line_style(2, line_segment=10, blank_segment=5)
665
# chart.set_title('heloooo')
666
# chart.set_legend(('sine wave', 'random * x'))
667
# chart.set_colours(('ee2000', 'DDDDAA', 'fF03f2'))
668
# chart.fill_solid(Chart.BACKGROUND, '123456')
669
# chart.fill_linear_gradient(Chart.CHART, 20, '004070', 1, '300040', 0,
671
# chart.fill_linear_stripes(Chart.CHART, 20, '204070', .2, '300040', .2,
673
chart.set_axis_range(Axis.LEFT, 0, 10)
674
chart.set_axis_range(Axis.RIGHT, 5, 30)
675
chart.set_axis_labels(Axis.BOTTOM, [1, 25, 95])
676
chart.set_axis_positions(Axis.BOTTOM, [1, 25, 95])
677
chart.set_axis_style(Axis.BOTTOM, 'FFFFFF', 15)
679
# chart.set_pie_labels(('apples', 'oranges', 'bananas'))
681
# chart.set_grid(10, 10)
683
# for a in xrange(0, 100, 10):
684
# chart.add_marker(1, a, 'a', 'AACA20', 10)
686
chart.add_horizontal_range('00A020', .2, .5)
687
chart.add_vertical_range('00c030', .2, .4)
689
chart.add_fill_simple('303030A0')
691
chart.download('test.png')
693
url = chart.get_url()
696
data = urllib.urlopen(chart.get_url()).read()
697
open('meh.png', 'wb').write(data)
698
os.system('start meh.png')
701
if __name__ == '__main__':