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))
105
114
class TextData(Data):
107
116
def __repr__(self):
117
max_value = self.max_value()
108
118
encoded_data = []
109
119
for data in self.data:
111
121
for value in data:
112
122
if value is None:
113
123
sub_data.append(-1)
114
elif value >= 0 and value <= TextData.max_value:
115
sub_data.append(str(float(value)))
124
elif value >= 0 and value <= max_value:
125
sub_data.append("%.1f" % float(value))
117
127
raise DataOutOfRangeException()
118
128
encoded_data.append(','.join(sub_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))
126
143
class ExtendedData(Data):
128
145
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-.'
130
147
def __repr__(self):
148
max_value = self.max_value()
131
149
encoded_data = []
132
150
enc_size = len(ExtendedData.enc_map)
133
151
for data in self.data:
237
264
LINEAR_GRADIENT = 'lg'
238
265
LINEAR_STRIPES = 'ls'
240
def __init__(self, width, height, title=None, legend=None, colours=None):
267
def __init__(self, width, height, title=None, legend=None, colours=None,
268
auto_scale=True, x_range=None, y_range=None):
241
269
assert(type(self) != Chart) # This is an abstract class
242
270
assert(isinstance(width, int))
243
271
assert(isinstance(height, int))
384
421
# -------------------------------------------------------------------------
386
423
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.
424
"""Determines the appropriate data encoding type to give satisfactory
425
resolution (http://code.google.com/apis/chart/#chart_data).
391
427
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()
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)
402
512
def add_data(self, data):
403
513
self.data.append(data)
501
615
class ScatterChart(Chart):
503
def __init__(self, *args, **kwargs):
504
Chart.__init__(self, *args, **kwargs)
506
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])
510
628
class LineChart(Chart):
592
729
assert(type(self) != GroupedBarChart) # This is an abstract class
593
730
BarChart.__init__(self, *args, **kwargs)
594
731
self.bar_spacing = None
732
self.group_spacing = None
596
734
def set_bar_spacing(self, spacing):
735
"""Set spacing between bars in a group."""
597
736
self.bar_spacing = spacing
738
def set_group_spacing(self, spacing):
739
"""Set spacing between groups of bars."""
740
self.group_spacing = spacing
599
742
def get_url_bits(self):
600
743
# Skip 'BarChart.get_url_bits' and call Chart directly so the parent
601
744
# doesn't add "chbh" before we do.
602
745
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')
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')
607
759
url_bits.append('chbh=%i,%i' % (self.bar_width, self.bar_spacing))
609
761
url_bits.append('chbh=%i' % self.bar_width)