2
pygooglechart - A complete Python wrapper for the Google Chart API
4
http://pygooglechart.slowchop.com/
6
Copyright 2007-2009 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/>.
22
from __future__ import division
26
import urllib.request, urllib.error
33
# Helper variables and functions
34
# -----------------------------------------------------------------------------
37
__author__ = 'Gerald Kaszuba'
39
reo_colour = re.compile('^([A-Fa-f0-9]{2,2}){3,4}$')
41
def _check_colour(colour):
42
if not reo_colour.match(colour):
43
raise InvalidParametersException('Colours need to be in ' \
44
'RRGGBB or RRGGBBAA format. One of your colours has %s' % \
48
def _reset_warnings():
49
"""Helper function to reset all warnings. Used by the unit tests."""
50
globals()['__warningregistry__'] = None
54
# -----------------------------------------------------------------------------
57
class PyGoogleChartException(Exception):
61
class DataOutOfRangeException(PyGoogleChartException):
65
class UnknownDataTypeException(PyGoogleChartException):
69
class NoDataGivenException(PyGoogleChartException):
73
class InvalidParametersException(PyGoogleChartException):
77
class BadContentTypeException(PyGoogleChartException):
81
class AbstractClassException(PyGoogleChartException):
85
class UnknownChartType(PyGoogleChartException):
88
class UnknownCountryCodeException(PyGoogleChartException):
92
# -----------------------------------------------------------------------------
97
def __init__(self, data):
98
if type(self) == Data:
99
raise AbstractClassException('This is an abstract class')
103
def float_scale_value(cls, value, range):
105
assert(upper > lower)
106
scaled = (value - lower) * (cls.max_value / (upper - lower))
110
def clip_value(cls, value):
111
return max(0, min(value, cls.max_value))
114
def int_scale_value(cls, value, range):
115
return int(round(cls.float_scale_value(value, range)))
118
def scale_value(cls, value, range):
119
scaled = cls.int_scale_value(value, range)
120
clipped = cls.clip_value(scaled)
121
Data.check_clip(scaled, clipped)
125
def check_clip(scaled, clipped):
126
if clipped != scaled:
127
warnings.warn('One or more of of your data points has been '
128
'clipped because it is out of range.')
131
class SimpleData(Data):
134
enc_map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
138
for data in self.data:
143
elif value >= 0 and value <= self.max_value:
144
sub_data.append(SimpleData.enc_map[value])
146
raise DataOutOfRangeException('cannot encode value: %d'
148
encoded_data.append(''.join(sub_data))
149
return 'chd=s:' + ','.join(encoded_data)
152
class TextData(Data):
158
for data in self.data:
163
elif value >= 0 and value <= self.max_value:
164
sub_data.append("%.1f" % float(value))
166
raise DataOutOfRangeException()
167
encoded_data.append(','.join(sub_data))
168
return 'chd=t:' + '%7c'.join(encoded_data)
171
def scale_value(cls, value, range):
172
# use float values instead of integers because we don't need an encode
174
scaled = cls.float_scale_value(value, range)
175
clipped = cls.clip_value(scaled)
176
Data.check_clip(scaled, clipped)
180
class ExtendedData(Data):
184
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-.'
188
enc_size = len(ExtendedData.enc_map)
189
for data in self.data:
193
sub_data.append('__')
194
elif value >= 0 and value <= self.max_value:
195
first, second = divmod(int(value), enc_size)
196
sub_data.append('%s%s' % (
197
ExtendedData.enc_map[first],
198
ExtendedData.enc_map[second]))
200
raise DataOutOfRangeException( \
201
'Item #%i "%s" is out of range' % (data.index(value), \
203
encoded_data.append(''.join(sub_data))
204
return 'chd=e:' + ','.join(encoded_data)
208
# -----------------------------------------------------------------------------
217
TYPES = (BOTTOM, TOP, LEFT, RIGHT)
219
def __init__(self, axis_index, axis_type, **kw):
220
assert(axis_type in Axis.TYPES)
221
self.has_style = False
222
self.axis_index = axis_index
223
self.axis_type = axis_type
224
self.positions = None
226
def set_index(self, axis_index):
227
self.axis_index = axis_index
229
def set_positions(self, positions):
230
self.positions = positions
232
def set_style(self, colour, font_size=None, alignment=None):
233
_check_colour(colour)
235
self.font_size = font_size
236
self.alignment = alignment
237
self.has_style = True
239
def style_to_url(self):
241
bits.append(str(self.axis_index))
242
bits.append(self.colour)
243
if self.font_size is not None:
244
bits.append(str(self.font_size))
245
if self.alignment is not None:
246
bits.append(str(self.alignment))
247
return ','.join(bits)
249
def positions_to_url(self):
251
bits.append(str(self.axis_index))
252
bits += [str(a) for a in self.positions]
253
return ','.join(bits)
256
class LabelAxis(Axis):
258
def __init__(self, axis_index, axis_type, values, **kwargs):
259
Axis.__init__(self, axis_index, axis_type, **kwargs)
260
self.values = [str(a) for a in values]
263
return '%i:%%7c%s' % (self.axis_index, '%7c'.join(self.values))
266
class RangeAxis(Axis):
268
def __init__(self, axis_index, axis_type, low, high, **kwargs):
269
Axis.__init__(self, axis_index, axis_type, **kwargs)
274
return '%i,%s,%s' % (self.axis_index, self.low, self.high)
277
# -----------------------------------------------------------------------------
281
"""Abstract class for all chart types.
283
width are height specify the dimensions of the image. title sets the title
284
of the chart. legend requires a list that corresponds to datasets.
287
BASE_URL = 'http://chart.apis.google.com/chart'
291
VALID_SOLID_FILL_TYPES = (BACKGROUND, CHART, ALPHA)
293
LINEAR_GRADIENT = 'lg'
294
LINEAR_STRIPES = 'ls'
296
def __init__(self, width, height, title=None, legend=None, colours=None,
297
auto_scale=True, x_range=None, y_range=None,
298
colours_within_series=None):
299
if type(self) == Chart:
300
raise AbstractClassException('This is an abstract class')
301
assert(isinstance(width, int))
302
assert(isinstance(height, int))
306
self.set_title(title)
307
self.set_title_style(None, None)
308
self.set_legend(legend)
309
self.set_legend_position(None)
310
self.set_colours(colours)
311
self.set_colours_within_series(colours_within_series)
314
self.auto_scale = auto_scale # Whether to automatically scale data
315
self.x_range = x_range # (min, max) x-axis range for scaling
316
self.y_range = y_range # (min, max) y-axis range for scaling
317
self.scaled_data_class = None
318
self.scaled_x_range = None
319
self.scaled_y_range = None
322
Chart.BACKGROUND: None,
327
Chart.BACKGROUND: None,
333
self.line_styles = {}
335
self.title_colour = None
336
self.title_font_size = None
339
# -------------------------------------------------------------------------
341
def get_url(self, data_class=None):
342
return self.BASE_URL + '?' + self.get_url_extension(data_class)
344
def get_url_extension(self, data_class=None):
345
url_bits = self.get_url_bits(data_class=data_class)
346
return '&'.join(url_bits)
348
def get_url_bits(self, data_class=None):
351
url_bits.append(self.type_to_url())
352
url_bits.append('chs=%ix%i' % (self.width, self.height))
353
url_bits.append(self.data_to_url(data_class=data_class))
356
url_bits.append('chtt=%s' % self.title)
357
if self.title_colour and self.title_font_size:
358
url_bits.append('chts=%s,%s' % (self.title_colour, \
359
self.title_font_size))
361
url_bits.append('chdl=%s' % '%7c'.join(self.legend))
362
if self.legend_position:
363
url_bits.append('chdlp=%s' % (self.legend_position))
365
url_bits.append('chco=%s' % ','.join(self.colours))
366
if self.colours_within_series:
367
url_bits.append('chco=%s' % '%7c'.join(self.colours_within_series))
368
ret = self.fill_to_url()
371
ret = self.axis_to_url()
375
url_bits.append(self.markers_to_url())
378
for index in range(max(self.line_styles) + 1):
379
if index in self.line_styles:
380
values = self.line_styles[index]
383
style.append(','.join(values))
384
url_bits.append('chls=%s' % '%7c'.join(style))
386
url_bits.append('chg=%s' % self.grid)
390
# -------------------------------------------------------------------------
392
def download(self, file_name, use_post=True):
394
opener = urllib.request.urlopen(self.BASE_URL, self.get_url_extension())
396
opener = urllib.request.urlopen(self.get_url())
398
if opener.headers['content-type'] != 'image/png':
399
raise BadContentTypeException('Server responded with a ' \
400
'content-type of %s' % opener.headers['content-type'])
402
open(file_name, 'wb').write(opener.read())
405
# -------------------------------------------------------------------------
407
def set_title(self, title):
409
self.title = urllib.parse.quote(title)
413
def set_title_style(self, colour=None, font_size=None):
414
if not colour is None:
415
_check_colour(colour)
416
if not colour and not font_size:
418
self.title_colour = colour or '333333'
419
self.title_font_size = font_size or 13.5
421
def set_legend(self, legend):
422
"""legend needs to be a list, tuple or None"""
423
assert(isinstance(legend, list) or isinstance(legend, tuple) or
426
self.legend = [urllib.parse.quote(a) for a in legend]
430
def set_legend_position(self, legend_position):
432
self.legend_position = urllib.parse.quote(legend_position)
434
self.legend_position = None
437
# -------------------------------------------------------------------------
439
def set_colours(self, colours):
440
# colours needs to be a list, tuple or None
441
assert(isinstance(colours, list) or isinstance(colours, tuple) or
443
# make sure the colours are in the right format
447
self.colours = colours
449
def set_colours_within_series(self, colours):
450
# colours needs to be a list, tuple or None
451
assert(isinstance(colours, list) or isinstance(colours, tuple) or
453
# make sure the colours are in the right format
457
self.colours_within_series = colours
459
# Background/Chart colours
460
# -------------------------------------------------------------------------
462
def fill_solid(self, area, colour):
463
assert(area in Chart.VALID_SOLID_FILL_TYPES)
464
_check_colour(colour)
465
self.fill_area[area] = colour
466
self.fill_types[area] = Chart.SOLID
468
def _check_fill_linear(self, angle, *args):
469
assert(isinstance(args, list) or isinstance(args, tuple))
470
assert(angle >= 0 and angle <= 90)
471
assert(len(args) % 2 == 0)
472
args = list(args) # args is probably a tuple and we need to mutate
473
for a in range(int(len(args) / 2)):
475
offset = args[a * 2 + 1]
477
assert(offset >= 0 and offset <= 1)
478
args[a * 2 + 1] = str(args[a * 2 + 1])
481
def fill_linear_gradient(self, area, angle, *args):
482
assert(area in Chart.VALID_SOLID_FILL_TYPES)
483
args = self._check_fill_linear(angle, *args)
484
self.fill_types[area] = Chart.LINEAR_GRADIENT
485
self.fill_area[area] = ','.join([str(angle)] + args)
487
def fill_linear_stripes(self, area, angle, *args):
488
assert(area in Chart.VALID_SOLID_FILL_TYPES)
489
args = self._check_fill_linear(angle, *args)
490
self.fill_types[area] = Chart.LINEAR_STRIPES
491
self.fill_area[area] = ','.join([str(angle)] + args)
493
def fill_to_url(self):
495
for area in (Chart.BACKGROUND, Chart.CHART, Chart.ALPHA):
496
if self.fill_types[area]:
497
areas.append('%s,%s,%s' % (area, self.fill_types[area], \
498
self.fill_area[area]))
500
return 'chf=' + '%7c'.join(areas)
503
# -------------------------------------------------------------------------
505
def data_class_detection(self, data):
506
"""Determines the appropriate data encoding type to give satisfactory
507
resolution (http://code.google.com/apis/chart/#chart_data).
509
assert(isinstance(data, list) or isinstance(data, tuple))
510
if not isinstance(self, (LineChart, BarChart, ScatterChart)):
511
# From the link above:
512
# Simple encoding is suitable for all other types of chart
513
# regardless of size.
515
elif self.height < 100:
516
# The link above indicates that line and bar charts less
517
# than 300px in size can be suitably represented with the
518
# simple encoding. I've found that this isn't sufficient,
519
# e.g. examples/line-xy-circle.png. Let's try 100px.
524
def _filter_none(self, data):
525
return [r for r in data if r is not None]
527
def data_x_range(self):
528
"""Return a 2-tuple giving the minimum and maximum x-axis
532
lower = min([min(self._filter_none(s))
533
for type, s in self.annotated_data()
535
upper = max([max(self._filter_none(s))
536
for type, s in self.annotated_data()
538
return (lower, upper)
540
return None # no x-axis datasets
542
def data_y_range(self):
543
"""Return a 2-tuple giving the minimum and maximum y-axis
547
lower = min([min(self._filter_none(s))
548
for type, s in self.annotated_data()
550
upper = max([max(self._filter_none(s)) + 1
551
for type, s in self.annotated_data()
553
return (lower, upper)
555
return None # no y-axis datasets
557
def scaled_data(self, data_class, x_range=None, y_range=None):
558
"""Scale `self.data` as appropriate for the given data encoding
559
(data_class) and return it.
561
An optional `y_range` -- a 2-tuple (lower, upper) -- can be
562
given to specify the y-axis bounds. If not given, the range is
563
inferred from the data: (0, <max-value>) presuming no negative
564
values, or (<min-value>, <max-value>) if there are negative
565
values. `self.scaled_y_range` is set to the actual lower and
568
Ditto for `x_range`. Note that some chart types don't have x-axis
571
self.scaled_data_class = data_class
573
# Determine the x-axis range for scaling.
575
x_range = self.data_x_range()
576
if x_range and x_range[0] > 0:
577
x_range = (x_range[0], x_range[1])
578
self.scaled_x_range = x_range
580
# Determine the y-axis range for scaling.
582
y_range = self.data_y_range()
583
if y_range and y_range[0] > 0:
584
y_range = (y_range[0], y_range[1])
585
self.scaled_y_range = y_range
588
for type, dataset in self.annotated_data():
590
scale_range = x_range
592
scale_range = y_range
593
elif type == 'marker-size':
594
scale_range = (0, max(dataset))
598
scaled_dataset.append(None)
600
scaled_dataset.append(
601
data_class.scale_value(v, scale_range))
602
scaled_data.append(scaled_dataset)
605
def add_data(self, data):
606
self.data.append(data)
607
return len(self.data) - 1 # return the "index" of the data set
609
def data_to_url(self, data_class=None):
611
data_class = self.data_class_detection(self.data)
612
if not issubclass(data_class, Data):
613
raise UnknownDataTypeException()
615
data = self.scaled_data(data_class, self.x_range, self.y_range)
618
return repr(data_class(data))
620
def annotated_data(self):
621
for dataset in self.data:
625
# -------------------------------------------------------------------------
627
def set_axis_labels(self, axis_type, values):
628
assert(axis_type in Axis.TYPES)
629
values = [urllib.parse.quote(str(a)) for a in values]
630
axis_index = len(self.axis)
631
axis = LabelAxis(axis_index, axis_type, values)
632
self.axis.append(axis)
635
def set_axis_range(self, axis_type, low, high):
636
assert(axis_type in Axis.TYPES)
637
axis_index = len(self.axis)
638
axis = RangeAxis(axis_index, axis_type, low, high)
639
self.axis.append(axis)
642
def set_axis_positions(self, axis_index, positions):
644
self.axis[axis_index].set_positions(positions)
646
raise InvalidParametersException('Axis index %i has not been ' \
649
def set_axis_style(self, axis_index, colour, font_size=None, \
652
self.axis[axis_index].set_style(colour, font_size, alignment)
654
raise InvalidParametersException('Axis index %i has not been ' \
657
def axis_to_url(self):
664
for axis in self.axis:
665
available_axis.append(axis.axis_type)
666
if isinstance(axis, RangeAxis):
667
range_axis.append(repr(axis))
668
if isinstance(axis, LabelAxis):
669
label_axis.append(repr(axis))
671
positions.append(axis.positions_to_url())
673
styles.append(axis.style_to_url())
674
if not available_axis:
677
url_bits.append('chxt=%s' % ','.join(available_axis))
679
url_bits.append('chxl=%s' % '%7c'.join(label_axis))
681
url_bits.append('chxr=%s' % '%7c'.join(range_axis))
683
url_bits.append('chxp=%s' % '%7c'.join(positions))
685
url_bits.append('chxs=%s' % '%7c'.join(styles))
686
return '&'.join(url_bits)
688
# Markers, Ranges and Fill area (chm)
689
# -------------------------------------------------------------------------
691
def markers_to_url(self):
692
return 'chm=%s' % '%7c'.join([','.join(a) for a in self.markers])
694
def add_marker(self, index, point, marker_type, colour, size, priority=0):
695
self.markers.append((marker_type, colour, str(index), str(point), \
696
str(size), str(priority)))
698
def add_horizontal_range(self, colour, start, stop):
699
self.markers.append(('r', colour, '0', str(start), str(stop)))
701
def add_data_line(self, colour, data_set, size, priority=0):
702
self.markers.append(('D', colour, str(data_set), '0', str(size), \
705
def add_marker_text(self, string, colour, data_set, data_point, size, \
707
self.markers.append((str(string), colour, str(data_set), \
708
str(data_point), str(size), str(priority)))
710
def add_vertical_range(self, colour, start, stop):
711
self.markers.append(('R', colour, '0', str(start), str(stop)))
713
def add_fill_range(self, colour, index_start, index_end):
714
self.markers.append(('b', colour, str(index_start), str(index_end), \
717
def add_fill_simple(self, colour):
718
self.markers.append(('B', colour, '1', '1', '1'))
721
# -------------------------------------------------------------------------
723
def set_line_style(self, index, thickness=1, line_segment=None, \
726
value.append(str(thickness))
728
value.append(str(line_segment))
729
value.append(str(blank_segment))
730
self.line_styles[index] = value
733
# -------------------------------------------------------------------------
735
def set_grid(self, x_step, y_step, line_segment=1, \
737
self.grid = '%s,%s,%s,%s' % (x_step, y_step, line_segment, \
741
class ScatterChart(Chart):
743
def type_to_url(self):
746
def annotated_data(self):
747
yield ('x', self.data[0])
748
yield ('y', self.data[1])
749
if len(self.data) > 2:
750
# The optional third dataset is relative sizing for point
752
yield ('marker-size', self.data[2])
755
class LineChart(Chart):
757
def __init__(self, *args, **kwargs):
758
if type(self) == LineChart:
759
raise AbstractClassException('This is an abstract class')
760
Chart.__init__(self, *args, **kwargs)
763
class SimpleLineChart(LineChart):
765
def type_to_url(self):
768
def annotated_data(self):
769
# All datasets are y-axis data.
770
for dataset in self.data:
774
class SparkLineChart(SimpleLineChart):
776
def type_to_url(self):
780
class XYLineChart(LineChart):
782
def type_to_url(self):
785
def annotated_data(self):
786
# Datasets alternate between x-axis, y-axis.
787
for i, dataset in enumerate(self.data):
794
class BarChart(Chart):
796
def __init__(self, *args, **kwargs):
797
if type(self) == BarChart:
798
raise AbstractClassException('This is an abstract class')
799
Chart.__init__(self, *args, **kwargs)
800
self.bar_width = None
803
def set_bar_width(self, bar_width):
804
self.bar_width = bar_width
806
def set_zero_line(self, index, zero_line):
807
self.zero_lines[index] = zero_line
809
def get_url_bits(self, data_class=None, skip_chbh=False):
810
url_bits = Chart.get_url_bits(self, data_class=data_class)
811
if not skip_chbh and self.bar_width is not None:
812
url_bits.append('chbh=%i' % self.bar_width)
815
for index in range(max(self.zero_lines) + 1):
816
if index in self.zero_lines:
817
zero_line.append(str(self.zero_lines[index]))
819
zero_line.append('0')
820
url_bits.append('chp=%s' % ','.join(zero_line))
824
class StackedHorizontalBarChart(BarChart):
826
def type_to_url(self):
830
class StackedVerticalBarChart(BarChart):
832
def type_to_url(self):
835
def annotated_data(self):
836
for dataset in self.data:
840
class GroupedBarChart(BarChart):
842
def __init__(self, *args, **kwargs):
843
if type(self) == GroupedBarChart:
844
raise AbstractClassException('This is an abstract class')
845
BarChart.__init__(self, *args, **kwargs)
846
self.bar_spacing = None
847
self.group_spacing = None
849
def set_bar_spacing(self, spacing):
850
"""Set spacing between bars in a group."""
851
self.bar_spacing = spacing
853
def set_group_spacing(self, spacing):
854
"""Set spacing between groups of bars."""
855
self.group_spacing = spacing
857
def get_url_bits(self, data_class=None):
858
# Skip 'BarChart.get_url_bits' and call Chart directly so the parent
859
# doesn't add "chbh" before we do.
860
url_bits = BarChart.get_url_bits(self, data_class=data_class,
862
if self.group_spacing is not None:
863
if self.bar_spacing is None:
864
raise InvalidParametersException('Bar spacing is required ' \
865
'to be set when setting group spacing')
866
if self.bar_width is None:
867
raise InvalidParametersException('Bar width is required to ' \
868
'be set when setting bar spacing')
869
url_bits.append('chbh=%i,%i,%i'
870
% (self.bar_width, self.bar_spacing, self.group_spacing))
871
elif self.bar_spacing is not None:
872
if self.bar_width is None:
873
raise InvalidParametersException('Bar width is required to ' \
874
'be set when setting bar spacing')
875
url_bits.append('chbh=%i,%i' % (self.bar_width, self.bar_spacing))
877
url_bits.append('chbh=%i' % self.bar_width)
881
class GroupedHorizontalBarChart(GroupedBarChart):
883
def type_to_url(self):
887
class GroupedVerticalBarChart(GroupedBarChart):
889
def type_to_url(self):
892
def annotated_data(self):
893
for dataset in self.data:
897
class PieChart(Chart):
899
def __init__(self, *args, **kwargs):
900
if type(self) == PieChart:
901
raise AbstractClassException('This is an abstract class')
902
Chart.__init__(self, *args, **kwargs)
905
warnings.warn('y_range is not used with %s.' % \
906
(self.__class__.__name__))
908
def set_pie_labels(self, labels):
909
self.pie_labels = [urllib.parse.quote(a) for a in labels]
911
def get_url_bits(self, data_class=None):
912
url_bits = Chart.get_url_bits(self, data_class=data_class)
914
url_bits.append('chl=%s' % '%7c'.join(self.pie_labels))
917
def annotated_data(self):
918
# Datasets are all y-axis data. However, there should only be
919
# one dataset for pie charts.
920
for dataset in self.data:
923
def scaled_data(self, data_class, x_range=None, y_range=None):
925
x_range = [0, sum(self.data[0])]
926
return Chart.scaled_data(self, data_class, x_range, self.y_range)
929
class PieChart2D(PieChart):
931
def type_to_url(self):
935
class PieChart3D(PieChart):
937
def type_to_url(self):
941
class VennChart(Chart):
943
def type_to_url(self):
946
def annotated_data(self):
947
for dataset in self.data:
951
class RadarChart(Chart):
953
def type_to_url(self):
957
class SplineRadarChart(RadarChart):
959
def type_to_url(self):
963
class MapChart(Chart):
965
def __init__(self, *args, **kwargs):
966
Chart.__init__(self, *args, **kwargs)
967
self.geo_area = 'world'
969
self.__areas = ('africa', 'asia', 'europe', 'middle_east',
970
'south_america', 'usa', 'world')
972
'AD', 'AE', 'AF', 'AG', 'AI', 'AL', 'AM', 'AN', 'AO', 'AQ', 'AR',
973
'AS', 'AT', 'AU', 'AW', 'AX', 'AZ', 'BA', 'BB', 'BD', 'BE', 'BF',
974
'BG', 'BH', 'BI', 'BJ', 'BL', 'BM', 'BN', 'BO', 'BR', 'BS', 'BT',
975
'BV', 'BW', 'BY', 'BZ', 'CA', 'CC', 'CD', 'CF', 'CG', 'CH', 'CI',
976
'CK', 'CL', 'CM', 'CN', 'CO', 'CR', 'CU', 'CV', 'CX', 'CY', 'CZ',
977
'DE', 'DJ', 'DK', 'DM', 'DO', 'DZ', 'EC', 'EE', 'EG', 'EH', 'ER',
978
'ES', 'ET', 'FI', 'FJ', 'FK', 'FM', 'FO', 'FR', 'GA', 'GB', 'GD',
979
'GE', 'GF', 'GG', 'GH', 'GI', 'GL', 'GM', 'GN', 'GP', 'GQ', 'GR',
980
'GS', 'GT', 'GU', 'GW', 'GY', 'HK', 'HM', 'HN', 'HR', 'HT', 'HU',
981
'ID', 'IE', 'IL', 'IM', 'IN', 'IO', 'IQ', 'IR', 'IS', 'IT', 'JE',
982
'JM', 'JO', 'JP', 'KE', 'KG', 'KH', 'KI', 'KM', 'KN', 'KP', 'KR',
983
'KW', 'KY', 'KZ', 'LA', 'LB', 'LC', 'LI', 'LK', 'LR', 'LS', 'LT',
984
'LU', 'LV', 'LY', 'MA', 'MC', 'MD', 'ME', 'MF', 'MG', 'MH', 'MK',
985
'ML', 'MM', 'MN', 'MO', 'MP', 'MQ', 'MR', 'MS', 'MT', 'MU', 'MV',
986
'MW', 'MX', 'MY', 'MZ', 'NA', 'NC', 'NE', 'NF', 'NG', 'NI', 'NL',
987
'NO', 'NP', 'NR', 'NU', 'NZ', 'OM', 'PA', 'PE', 'PF', 'PG', 'PH',
988
'PK', 'PL', 'PM', 'PN', 'PR', 'PS', 'PT', 'PW', 'PY', 'QA', 'RE',
989
'RO', 'RS', 'RU', 'RW', 'SA', 'SB', 'SC', 'SD', 'SE', 'SG', 'SH',
990
'SI', 'SJ', 'SK', 'SL', 'SM', 'SN', 'SO', 'SR', 'ST', 'SV', 'SY',
991
'SZ', 'TC', 'TD', 'TF', 'TG', 'TH', 'TJ', 'TK', 'TL', 'TM', 'TN',
992
'TO', 'TR', 'TT', 'TV', 'TW', 'TZ', 'UA', 'UG', 'UM', 'US', 'UY',
993
'UZ', 'VA', 'VC', 'VE', 'VG', 'VI', 'VN', 'VU', 'WF', 'WS', 'YE',
994
'YT', 'ZA', 'ZM', 'ZW')
996
def type_to_url(self):
999
def set_codes(self, codes):
1000
'''Set the country code map for the data.
1001
Codes given in a list.
1012
if cc in self.__ccodes:
1015
raise UnknownCountryCodeException(cc)
1017
self.codes = codemap
1019
def set_geo_area(self, area):
1020
'''Sets the geo area for the map.
1031
if area in self.__areas:
1032
self.geo_area = area
1034
raise UnknownChartType('Unknown chart type for maps: %s' %area)
1036
def get_url_bits(self, data_class=None):
1037
url_bits = Chart.get_url_bits(self, data_class=data_class)
1038
url_bits.append('chtm=%s' % self.geo_area)
1040
url_bits.append('chld=%s' % ''.join(self.codes))
1043
def add_data_dict(self, datadict):
1044
'''Sets the data and country codes via a dictionary.
1046
i.e. {'DE': 50, 'GB': 30, 'AT': 70}
1049
self.set_codes(datadict.keys())
1050
self.add_data(datadict.values())
1053
class GoogleOMeterChart(PieChart):
1054
"""Inheriting from PieChart because of similar labeling"""
1056
def __init__(self, *args, **kwargs):
1057
PieChart.__init__(self, *args, **kwargs)
1058
if self.auto_scale and not self.x_range:
1059
warnings.warn('Please specify an x_range with GoogleOMeterChart, '
1060
'otherwise one arrow will always be at the max.')
1062
def type_to_url(self):
1066
class QRChart(Chart):
1068
def __init__(self, *args, **kwargs):
1069
Chart.__init__(self, *args, **kwargs)
1070
self.encoding = None
1071
self.ec_level = None
1074
def type_to_url(self):
1077
def data_to_url(self, data_class=None):
1079
raise NoDataGivenException()
1080
return 'chl=%s' % urllib.parse.quote(self.data[0])
1082
def get_url_bits(self, data_class=None):
1083
url_bits = Chart.get_url_bits(self, data_class=data_class)
1085
url_bits.append('choe=%s' % self.encoding)
1087
url_bits.append('chld=%s%%7c%s' % (self.ec_level, self.margin))
1090
def set_encoding(self, encoding):
1091
self.encoding = encoding
1093
def set_ec(self, level, margin):
1094
self.ec_level = level
1095
self.margin = margin
1098
class ChartGrammar(object):
1104
def parse(self, grammar):
1105
self.grammar = grammar
1106
self.chart = self.create_chart_instance()
1108
for attr in self.grammar:
1109
if attr in ('w', 'h', 'type', 'auto_scale', 'x_range', 'y_range'):
1110
continue # These are already parsed in create_chart_instance
1111
attr_func = 'parse_' + attr
1112
if not hasattr(self, attr_func):
1113
warnings.warn('No parser for grammar attribute "%s"' % (attr))
1115
getattr(self, attr_func)(grammar[attr])
1119
def parse_data(self, data):
1120
self.chart.data = data
1123
def get_possible_chart_types():
1124
possible_charts = []
1125
for cls_name in globals().keys():
1126
if not cls_name.endswith('Chart'):
1128
cls = globals()[cls_name]
1129
# Check if it is an abstract class
1131
a = cls(1, 1, auto_scale=False)
1133
except AbstractClassException:
1136
possible_charts.append(cls_name[:-5])
1137
return possible_charts
1139
def create_chart_instance(self, grammar=None):
1141
grammar = self.grammar
1142
assert(isinstance(grammar, dict)) # grammar must be a dict
1143
assert('w' in grammar) # width is required
1144
assert('h' in grammar) # height is required
1145
assert('type' in grammar) # type is required
1146
chart_type = grammar['type']
1149
auto_scale = grammar.get('auto_scale', None)
1150
x_range = grammar.get('x_range', None)
1151
y_range = grammar.get('y_range', None)
1152
types = ChartGrammar.get_possible_chart_types()
1153
if chart_type not in types:
1154
raise UnknownChartType('%s is an unknown chart type. Possible '
1155
'chart types are %s' % (chart_type, ','.join(types)))
1156
return globals()[chart_type + 'Chart'](w, h, auto_scale=auto_scale,
1157
x_range=x_range, y_range=y_range)
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'
87
max_value = self.max_value()
89
for data in self.data:
94
elif value >= 0 and value <= max_value:
95
sub_data.append(SimpleData.enc_map[value])
97
raise DataOutOfRangeException('cannot encode value: %d'
99
encoded_data.append(''.join(sub_data))
100
return 'chd=s:' + ','.join(encoded_data)
107
def scale_value(cls, value, range):
109
max_value = cls.max_value()
110
scaled = int(round((float(value) - lower) * max_value / upper))
111
clipped = max(0, min(scaled, max_value))
114
class TextData(Data):
117
max_value = self.max_value()
119
for data in self.data:
124
elif value >= 0 and value <= max_value:
125
sub_data.append("%.1f" % float(value))
127
raise DataOutOfRangeException()
128
encoded_data.append(','.join(sub_data))
129
return 'chd=t:' + '|'.join(encoded_data)
136
def scale_value(cls, value, range):
138
max_value = cls.max_value()
139
scaled = (float(value) - lower) * max_value / upper
140
clipped = max(0, min(scaled, max_value))
143
class ExtendedData(Data):
145
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-.'
148
max_value = self.max_value()
150
enc_size = len(ExtendedData.enc_map)
151
for data in self.data:
155
sub_data.append('__')
156
elif value >= 0 and value <= max_value:
157
first, second = divmod(int(value), enc_size)
158
sub_data.append('%s%s' % (
159
ExtendedData.enc_map[first],
160
ExtendedData.enc_map[second]))
162
raise DataOutOfRangeException( \
163
'Item #%i "%s" is out of range' % (data.index(value), \
165
encoded_data.append(''.join(sub_data))
166
return 'chd=e:' + ','.join(encoded_data)
173
def scale_value(cls, value, range):
175
max_value = cls.max_value()
176
scaled = int(round((float(value) - lower) * max_value / upper))
177
clipped = max(0, min(scaled, max_value))
182
# -----------------------------------------------------------------------------
190
TYPES = (BOTTOM, TOP, LEFT, RIGHT)
192
def __init__(self, axis_index, axis_type, **kw):
193
assert(axis_type in Axis.TYPES)
194
self.has_style = False
195
self.axis_index = axis_index
196
self.axis_type = axis_type
197
self.positions = None
199
def set_index(self, axis_index):
200
self.axis_index = axis_index
202
def set_positions(self, positions):
203
self.positions = positions
205
def set_style(self, colour, font_size=None, alignment=None):
206
_check_colour(colour)
208
self.font_size = font_size
209
self.alignment = alignment
210
self.has_style = True
212
def style_to_url(self):
214
bits.append(str(self.axis_index))
215
bits.append(self.colour)
216
if self.font_size is not None:
217
bits.append(str(self.font_size))
218
if self.alignment is not None:
219
bits.append(str(self.alignment))
220
return ','.join(bits)
222
def positions_to_url(self):
224
bits.append(str(self.axis_index))
225
bits += [str(a) for a in self.positions]
226
return ','.join(bits)
229
class LabelAxis(Axis):
231
def __init__(self, axis_index, axis_type, values, **kwargs):
232
Axis.__init__(self, axis_index, axis_type, **kwargs)
233
self.values = [str(a) for a in values]
236
return '%i:|%s' % (self.axis_index, '|'.join(self.values))
239
class RangeAxis(Axis):
241
def __init__(self, axis_index, axis_type, low, high, **kwargs):
242
Axis.__init__(self, axis_index, axis_type, **kwargs)
247
return '%i,%s,%s' % (self.axis_index, self.low, self.high)
250
# -----------------------------------------------------------------------------
254
"""Abstract class for all chart types.
256
width are height specify the dimensions of the image. title sets the title
257
of the chart. legend requires a list that corresponds to datasets.
260
BASE_URL = 'http://chart.apis.google.com/chart?'
264
LINEAR_GRADIENT = 'lg'
265
LINEAR_STRIPES = 'ls'
267
def __init__(self, width, height, title=None, legend=None, colours=None,
268
auto_scale=True, x_range=None, y_range=None):
269
assert(type(self) != Chart) # This is an abstract class
270
assert(isinstance(width, int))
271
assert(isinstance(height, int))
275
self.set_title(title)
276
self.set_legend(legend)
277
self.set_colours(colours)
280
self.auto_scale = auto_scale # Whether to automatically scale data
281
self.x_range = x_range # (min, max) x-axis range for scaling
282
self.y_range = y_range # (min, max) y-axis range for scaling
283
self.scaled_data_class = None
284
self.scaled_x_range = None
285
self.scaled_y_range = None
288
Chart.BACKGROUND: None,
292
Chart.BACKGROUND: None,
305
# -------------------------------------------------------------------------
308
url_bits = self.get_url_bits()
309
return self.BASE_URL + '&'.join(url_bits)
311
def get_url_bits(self):
314
url_bits.append(self.type_to_url())
315
url_bits.append('chs=%ix%i' % (self.width, self.height))
316
url_bits.append(self.data_to_url())
319
url_bits.append('chtt=%s' % self.title)
321
url_bits.append('chdl=%s' % '|'.join(self.legend))
323
url_bits.append('chco=%s' % ','.join(self.colours))
324
ret = self.fill_to_url()
327
ret = self.axis_to_url()
331
url_bits.append(self.markers_to_url())
335
# -------------------------------------------------------------------------
337
def download(self, file_name):
338
opener = urllib2.urlopen(self.get_url())
340
if opener.headers['content-type'] != 'image/png':
341
raise BadContentTypeException('Server responded with a ' \
342
'content-type of %s' % opener.headers['content-type'])
344
open(file_name, 'wb').write(urllib.urlopen(self.get_url()).read())
347
# -------------------------------------------------------------------------
349
def set_title(self, title):
351
self.title = urllib.quote(title)
355
def set_legend(self, legend):
356
# legend needs to be a list, tuple or None
357
assert(isinstance(legend, list) or isinstance(legend, tuple) or
360
self.legend = [urllib.quote(a) for a in legend]
365
# -------------------------------------------------------------------------
367
def set_colours(self, colours):
368
# colours needs to be a list, tuple or None
369
assert(isinstance(colours, list) or isinstance(colours, tuple) or
371
# make sure the colours are in the right format
375
self.colours = colours
377
# Background/Chart colours
378
# -------------------------------------------------------------------------
380
def fill_solid(self, area, colour):
381
assert(area in (Chart.BACKGROUND, Chart.CHART))
382
_check_colour(colour)
383
self.fill_area[area] = colour
384
self.fill_types[area] = Chart.SOLID
386
def _check_fill_linear(self, angle, *args):
387
assert(isinstance(args, list) or isinstance(args, tuple))
388
assert(angle >= 0 and angle <= 90)
389
assert(len(args) % 2 == 0)
390
args = list(args) # args is probably a tuple and we need to mutate
391
for a in xrange(len(args) / 2):
393
offset = args[a * 2 + 1]
395
assert(offset >= 0 and offset <= 1)
396
args[a * 2 + 1] = str(args[a * 2 + 1])
399
def fill_linear_gradient(self, area, angle, *args):
400
assert(area in (Chart.BACKGROUND, Chart.CHART))
401
args = self._check_fill_linear(angle, *args)
402
self.fill_types[area] = Chart.LINEAR_GRADIENT
403
self.fill_area[area] = ','.join([str(angle)] + args)
405
def fill_linear_stripes(self, area, angle, *args):
406
assert(area in (Chart.BACKGROUND, Chart.CHART))
407
args = self._check_fill_linear(angle, *args)
408
self.fill_types[area] = Chart.LINEAR_STRIPES
409
self.fill_area[area] = ','.join([str(angle)] + args)
411
def fill_to_url(self):
413
for area in (Chart.BACKGROUND, Chart.CHART):
414
if self.fill_types[area]:
415
areas.append('%s,%s,%s' % (area, self.fill_types[area], \
416
self.fill_area[area]))
418
return 'chf=' + '|'.join(areas)
421
# -------------------------------------------------------------------------
423
def data_class_detection(self, data):
424
"""Determines the appropriate data encoding type to give satisfactory
425
resolution (http://code.google.com/apis/chart/#chart_data).
427
assert(isinstance(data, list) or isinstance(data, tuple))
428
if not isinstance(self, (LineChart, BarChart, ScatterChart)):
429
# From the link above:
430
# Simple encoding is suitable for all other types of chart
431
# regardless of size.
433
elif self.height < 100:
434
# The link above indicates that line and bar charts less
435
# than 300px in size can be suitably represented with the
436
# simple encoding. I've found that this isn't sufficient,
437
# e.g. examples/line-xy-circle.png. Let's try 100px.
439
elif self.height < 500:
444
def data_x_range(self):
445
"""Return a 2-tuple giving the minimum and maximum x-axis
449
lower = min([min(s) for type, s in self.annotated_data()
451
upper = max([max(s) for type, s in self.annotated_data()
453
return (lower, upper)
455
return None # no x-axis datasets
457
def data_y_range(self):
458
"""Return a 2-tuple giving the minimum and maximum y-axis
462
lower = min([min(s) for type, s in self.annotated_data()
464
upper = max([max(s) for type, s in self.annotated_data()
466
return (lower, upper)
468
return None # no y-axis datasets
470
def scaled_data(self, data_class, x_range=None, y_range=None):
471
"""Scale `self.data` as appropriate for the given data encoding
472
(data_class) and return it.
474
An optional `y_range` -- a 2-tuple (lower, upper) -- can be
475
given to specify the y-axis bounds. If not given, the range is
476
inferred from the data: (0, <max-value>) presuming no negative
477
values, or (<min-value>, <max-value>) if there are negative
478
values. `self.scaled_y_range` is set to the actual lower and
481
Ditto for `x_range`. Note that some chart types don't have x-axis
484
self.scaled_data_class = data_class
486
# Determine the x-axis range for scaling.
488
x_range = self.data_x_range()
489
if x_range and x_range[0] > 0:
490
x_range = (0, x_range[1])
491
self.scaled_x_range = x_range
493
# Determine the y-axis range for scaling.
495
y_range = self.data_y_range()
496
if y_range and y_range[0] > 0:
497
y_range = (0, y_range[1])
498
self.scaled_y_range = y_range
501
for type, dataset in self.annotated_data():
503
scale_range = x_range
505
scale_range = y_range
506
elif type == 'marker-size':
507
scale_range = (0, max(dataset))
508
scaled_data.append([data_class.scale_value(v, scale_range)
512
def add_data(self, data):
513
self.data.append(data)
514
return len(self.data) - 1 # return the "index" of the data set
516
def data_to_url(self, data_class=None):
518
data_class = self.data_class_detection(self.data)
519
if not issubclass(data_class, Data):
520
raise UnknownDataTypeException()
522
data = self.scaled_data(data_class, self.x_range, self.y_range)
525
return repr(data_class(data))
528
# -------------------------------------------------------------------------
530
def set_axis_labels(self, axis_type, values):
531
assert(axis_type in Axis.TYPES)
532
values = [ urllib.quote(a) for a in values ]
533
axis_index = len(self.axis)
534
axis = LabelAxis(axis_index, axis_type, values)
535
self.axis.append(axis)
538
def set_axis_range(self, axis_type, low, high):
539
assert(axis_type in Axis.TYPES)
540
axis_index = len(self.axis)
541
axis = RangeAxis(axis_index, axis_type, low, high)
542
self.axis.append(axis)
545
def set_axis_positions(self, axis_index, positions):
547
self.axis[axis_index].set_positions(positions)
549
raise InvalidParametersException('Axis index %i has not been ' \
552
def set_axis_style(self, axis_index, colour, font_size=None, \
555
self.axis[axis_index].set_style(colour, font_size, alignment)
557
raise InvalidParametersException('Axis index %i has not been ' \
560
def axis_to_url(self):
567
for axis in self.axis:
568
available_axis.append(axis.axis_type)
569
if isinstance(axis, RangeAxis):
570
range_axis.append(repr(axis))
571
if isinstance(axis, LabelAxis):
572
label_axis.append(repr(axis))
574
positions.append(axis.positions_to_url())
576
styles.append(axis.style_to_url())
577
if not available_axis:
580
url_bits.append('chxt=%s' % ','.join(available_axis))
582
url_bits.append('chxl=%s' % '|'.join(label_axis))
584
url_bits.append('chxr=%s' % '|'.join(range_axis))
586
url_bits.append('chxp=%s' % '|'.join(positions))
588
url_bits.append('chxs=%s' % '|'.join(styles))
589
return '&'.join(url_bits)
591
# Markers, Ranges and Fill area (chm)
592
# -------------------------------------------------------------------------
594
def markers_to_url(self):
595
return 'chm=%s' % '|'.join([','.join(a) for a in self.markers])
597
def add_marker(self, index, point, marker_type, colour, size):
598
self.markers.append((marker_type, colour, str(index), str(point), \
601
def add_horizontal_range(self, colour, start, stop):
602
self.markers.append(('r', colour, '1', str(start), str(stop)))
604
def add_vertical_range(self, colour, start, stop):
605
self.markers.append(('R', colour, '1', str(start), str(stop)))
607
def add_fill_range(self, colour, index_start, index_end):
608
self.markers.append(('b', colour, str(index_start), str(index_end), \
611
def add_fill_simple(self, colour):
612
self.markers.append(('B', colour, '1', '1', '1'))
615
class ScatterChart(Chart):
617
def type_to_url(self):
620
def annotated_data(self):
621
yield ('x', self.data[0])
622
yield ('y', self.data[1])
623
if len(self.data) > 2:
624
# The optional third dataset is relative sizing for point
626
yield ('marker-size', self.data[2])
628
class LineChart(Chart):
630
def __init__(self, *args, **kwargs):
631
assert(type(self) != LineChart) # This is an abstract class
632
Chart.__init__(self, *args, **kwargs)
633
self.line_styles = {}
636
def set_line_style(self, index, thickness=1, line_segment=None, \
639
value.append(str(thickness))
641
value.append(str(line_segment))
642
value.append(str(blank_segment))
643
self.line_styles[index] = value
645
def set_grid(self, x_step, y_step, line_segment=1, \
647
self.grid = '%s,%s,%s,%s' % (x_step, y_step, line_segment, \
650
def get_url_bits(self):
651
url_bits = Chart.get_url_bits(self)
654
# for index, values in self.line_style.items():
655
for index in xrange(max(self.line_styles) + 1):
656
if index in self.line_styles:
657
values = self.line_styles[index]
660
style.append(','.join(values))
661
url_bits.append('chls=%s' % '|'.join(style))
663
url_bits.append('chg=%s' % self.grid)
667
class SimpleLineChart(LineChart):
669
def type_to_url(self):
672
def annotated_data(self):
673
# All datasets are y-axis data.
674
for dataset in self.data:
677
class XYLineChart(LineChart):
679
def type_to_url(self):
682
def annotated_data(self):
683
# Datasets alternate between x-axis, y-axis.
684
for i, dataset in enumerate(self.data):
690
class BarChart(Chart):
692
def __init__(self, *args, **kwargs):
693
assert(type(self) != BarChart) # This is an abstract class
694
Chart.__init__(self, *args, **kwargs)
695
self.bar_width = None
697
def set_bar_width(self, bar_width):
698
self.bar_width = bar_width
700
def get_url_bits(self):
701
url_bits = Chart.get_url_bits(self)
702
if self.bar_width is not None:
703
url_bits.append('chbh=%i' % self.bar_width)
707
class StackedHorizontalBarChart(BarChart):
709
def type_to_url(self):
712
def annotated_data(self):
713
for dataset in self.data:
716
class StackedVerticalBarChart(BarChart):
718
def type_to_url(self):
721
def annotated_data(self):
722
for dataset in self.data:
726
class GroupedBarChart(BarChart):
728
def __init__(self, *args, **kwargs):
729
assert(type(self) != GroupedBarChart) # This is an abstract class
730
BarChart.__init__(self, *args, **kwargs)
731
self.bar_spacing = None
732
self.group_spacing = None
734
def set_bar_spacing(self, spacing):
735
"""Set spacing between bars in a group."""
736
self.bar_spacing = spacing
738
def set_group_spacing(self, spacing):
739
"""Set spacing between groups of bars."""
740
self.group_spacing = spacing
742
def get_url_bits(self):
743
# Skip 'BarChart.get_url_bits' and call Chart directly so the parent
744
# doesn't add "chbh" before we do.
745
url_bits = Chart.get_url_bits(self)
746
if self.group_spacing is not None:
747
if self.bar_spacing is None:
748
raise InvalidParametersException('Bar spacing is required to ' \
749
'be set when setting group spacing')
750
if self.bar_width is None:
751
raise InvalidParametersException('Bar width is required to ' \
752
'be set when setting bar spacing')
753
url_bits.append('chbh=%i,%i,%i'
754
% (self.bar_width, self.bar_spacing, self.group_spacing))
755
elif self.bar_spacing is not None:
756
if self.bar_width is None:
757
raise InvalidParametersException('Bar width is required to ' \
758
'be set when setting bar spacing')
759
url_bits.append('chbh=%i,%i' % (self.bar_width, self.bar_spacing))
761
url_bits.append('chbh=%i' % self.bar_width)
765
class GroupedHorizontalBarChart(GroupedBarChart):
767
def type_to_url(self):
770
def annotated_data(self):
771
for dataset in self.data:
775
class GroupedVerticalBarChart(GroupedBarChart):
777
def type_to_url(self):
780
def annotated_data(self):
781
for dataset in self.data:
785
class PieChart(Chart):
787
def __init__(self, *args, **kwargs):
788
assert(type(self) != PieChart) # This is an abstract class
789
Chart.__init__(self, *args, **kwargs)
792
def set_pie_labels(self, labels):
793
self.pie_labels = [urllib.quote(a) for a in labels]
795
def get_url_bits(self):
796
url_bits = Chart.get_url_bits(self)
798
url_bits.append('chl=%s' % '|'.join(self.pie_labels))
801
def annotated_data(self):
802
# Datasets are all y-axis data. However, there should only be
803
# one dataset for pie charts.
804
for dataset in self.data:
808
class PieChart2D(PieChart):
810
def type_to_url(self):
814
class PieChart3D(PieChart):
816
def type_to_url(self):
820
class VennChart(Chart):
822
def type_to_url(self):
825
def annotated_data(self):
826
for dataset in self.data:
831
chart = GroupedVerticalBarChart(320, 200)
832
chart = PieChart2D(320, 200)
833
chart = ScatterChart(320, 200)
834
chart = SimpleLineChart(320, 200)
835
sine_data = [math.sin(float(a) / 10) * 2000 + 2000 for a in xrange(100)]
836
random_data = [a * random.random() * 30 for a in xrange(40)]
837
random_data2 = [random.random() * 4000 for a in xrange(10)]
838
# chart.set_bar_width(50)
839
# chart.set_bar_spacing(0)
840
chart.add_data(sine_data)
841
chart.add_data(random_data)
842
chart.add_data(random_data2)
843
# chart.set_line_style(1, thickness=2)
844
# chart.set_line_style(2, line_segment=10, blank_segment=5)
845
# chart.set_title('heloooo')
846
# chart.set_legend(('sine wave', 'random * x'))
847
# chart.set_colours(('ee2000', 'DDDDAA', 'fF03f2'))
848
# chart.fill_solid(Chart.BACKGROUND, '123456')
849
# chart.fill_linear_gradient(Chart.CHART, 20, '004070', 1, '300040', 0,
851
# chart.fill_linear_stripes(Chart.CHART, 20, '204070', .2, '300040', .2,
853
axis_left_index = chart.set_axis_range(Axis.LEFT, 0, 10)
854
axis_left_index = chart.set_axis_range(Axis.LEFT, 0, 10)
855
axis_left_index = chart.set_axis_range(Axis.LEFT, 0, 10)
856
axis_right_index = chart.set_axis_range(Axis.RIGHT, 5, 30)
857
axis_bottom_index = chart.set_axis_labels(Axis.BOTTOM, [1, 25, 95])
858
chart.set_axis_positions(axis_bottom_index, [1, 25, 95])
859
chart.set_axis_style(axis_bottom_index, '003050', 15)
861
# chart.set_pie_labels(('apples', 'oranges', 'bananas'))
863
# chart.set_grid(10, 10)
865
# for a in xrange(0, 100, 10):
866
# chart.add_marker(1, a, 'a', 'AACA20', 10)
868
chart.add_horizontal_range('00A020', .2, .5)
869
chart.add_vertical_range('00c030', .2, .4)
871
chart.add_fill_simple('303030A0')
873
chart.download('test.png')
875
url = chart.get_url()
878
data = urllib.urlopen(chart.get_url()).read()
879
open('meh.png', 'wb').write(data)
880
os.system('start meh.png')
883
if __name__ == '__main__':