Source code for awe.chart
import time
import six
from .view import Element, builtin
number_types = (int, float)
timed_tuple_types = (list, tuple)
to_ms = (lambda s: int(s * 1000))
now_ms = (lambda: to_ms(time.time()))
class Transformer(object):
key = 'base'
def add(self, existing_data, added_data):
transformed_added_data = self.transform(added_data)
for config in transformed_added_data.values():
added_series = config['series']
existing_series = existing_data.setdefault(config['title'], {
'title': config['title'],
'type': config['type'],
'series': []
})['series']
existing_series_set = set()
new_series_dict = {}
for series in existing_series:
existing_series_set.add(series['name'])
for series in added_series:
if series['name'] in existing_series_set:
new_series_dict[series['name']] = series['data']
else:
existing_series.append(series)
for series in existing_series:
if series['name'] in new_series_dict:
series['data'].extend(new_series_dict[series['name']])
return transformed_added_data
def transform(self, data):
raise NotImplementedError
@staticmethod
def _extract_timed_tuple(now, item):
if isinstance(item, timed_tuple_types) and len(item) == 2:
now, item = item
now = to_ms(now)
return now, item
[docs]class NoOpTransformer(Transformer):
"""
The default transformer.
Assumes data is already in appropriate highcharts format.
key: ``noop``
"""
key = 'noop'
def transform(self, data):
return data
[docs]class NumberSequenceTransformer(Transformer):
"""
A numbers transformer.
Assumes each item in data is a single number or a list of numbers.
If a list of numbers is supplied, each number in the list is assumed to belong to a different series.
key: ``numbers``
"""
key = 'numbers'
def transform(self, data):
now = now_ms()
series_dict = {}
for item in data:
now, item = self._extract_timed_tuple(now, item)
if isinstance(item, number_types):
item = [item]
for index, value in enumerate(item):
series_dict.setdefault(index, {'name': index + 1, 'data': []})['data'].append((now, value))
return {
'': {
'series': [series_dict[index] for index in sorted(series_dict)],
'title': '',
'type': 'line'
}
}
[docs]class FlatDictTransformer(Transformer):
key = 'flat'
def __init__(self, chart_mapping, series_mapping, value_key):
"""
A transformer that expects data items to be dictionaries.
key: ``flat``
:param chart_mapping: A list of keys to build charts from. (combinations of them)
:param series_mapping: A list of keys to build chart series from. (combinations of them)
:param value_key: The key to the value of the data item.
"""
self._chart_mapping = chart_mapping
self._series_mapping = series_mapping
self._value_key = value_key
def transform(self, data):
data = data or []
now = now_ms()
result = {}
chart_dict = {}
for item in data:
now, item = self._extract_timed_tuple(now, item)
chart_key = ' '.join(item[k] for k in self._chart_mapping)
series_key = ' '.join(item[k] for k in self._series_mapping)
value = item[self._value_key]
current_chart = chart_dict.setdefault(chart_key, {})
series_data = current_chart.setdefault(series_key, [])
series_data.append((now, value))
for chart_key, series in chart_dict.items():
for series_key, series_data in series.items():
(result.setdefault(chart_key, {
'series': [],
'title': chart_key,
'type': 'line'
})['series'].append({
'name': series_key,
'data': series_data
}))
return result
[docs]class DictLevelsTransformer(Transformer):
def __init__(self, chart_mapping, series_mapping):
"""
A transformer that handles nested dictionaries data items.
Usually instantiated by supplying a transform key in this format: ``[chart levels]to[series levels]``.
For example, a key of ``23to1`` assumes a "3 level" nested dictionary where the charts will be generated
from the different combinations of keys in the 2nd and 3rd levels and the series for each chart will be
generated from each key in the 1st level.
key: ``[Ns]to[Ms]``
"""
self._chart_mapping = chart_mapping
self._series_mapping = series_mapping
@staticmethod
def from_str(str_mapping):
split = str_mapping.split('to')
if len(split) == 2 and split[0].isdigit() and split[1].isdigit():
chart_mapping = [int(c) for c in split[0]]
series_mapping = [int(c) for c in split[1]]
return DictLevelsTransformer(chart_mapping, series_mapping)
return None
@property
def key(self):
from_key = ''.join(str(k) for k in self._chart_mapping)
to_key = ''.join(str(k) for k in self._series_mapping)
return '{}to{}'.format(from_key, to_key)
def transform(self, data):
data = data or []
now = now_ms()
result = {}
chart_dict = {}
for item in data:
now, item = self._extract_timed_tuple(now, item)
for path, value in self._iterate_paths(item, []):
self._process_path(chart_dict, now, path, value)
for chart_key, series in chart_dict.items():
for series_key, series_data in series.items():
(result.setdefault(chart_key, {
'series': [],
'title': chart_key,
'type': 'line'
})['series'].append({
'name': series_key,
'data': series_data
}))
return result
def _iterate_paths(self, level, current_path):
for level_key, level_value in level.items():
level_path = current_path[:] + [level_key]
if isinstance(level_value, dict):
for (inner_level_path, inner_level_value) in self._iterate_paths(level_value, level_path):
yield inner_level_path, inner_level_value
else:
yield level_path, level_value
def _process_path(self, chart_dict, now, path, value):
level_to_key = {i+1: level for i, level in enumerate(path)}
chart_key = ' '.join(level_to_key[k] for k in self._chart_mapping)
series_key = ' '.join(level_to_key[k] for k in self._series_mapping)
current_chart = chart_dict.setdefault(chart_key, {})
series_data = current_chart.setdefault(series_key, [])
series_data.append((now, value))
transformers = {t.key: t for t in [
NoOpTransformer(),
NumberSequenceTransformer(),
]}
transformer_classes = {t.key: t for t in [
FlatDictTransformer
]}
[docs]@builtin
class Chart(Element):
allow_children = False
_transformer = None
def _init(self, data=None, options=None, transform=None, moving_window=None):
self.transformer = transform
self.update_data({
'data': self.transformer.transform(data),
'options': options or {},
'movingWindow': moving_window
})
[docs] def add(self, data):
"""
Add new data to a chart after it has been created.
:param data: A list of data items. Each data item is expected to match the format the transformer expects.
A data item may also be supplied in the form of a 2-tuple (or a list) of (time, data),
in which case, the first item is the epoch time in seconds with ms precision and
the second item is the data item itself.
"""
transformed_data = self.transformer.add(self.data['data'], data)
self.update_element(
path=['data', 'data'],
action='addChartData',
data=transformed_data
)
[docs] def set(self, data):
"""
Override existing chart data with new data.
:param data: A list of data items. Each data item is expected to match the format the transformer expects.
A data item may also be supplied in the form of a 2-tuple (or a list) of (time, data),
in which case, the first item is the epoch time in seconds with ms precision and
the second item is the data item itself.
"""
transformed_data = self.transformer.transform(data)
self.data['data'] = transformed_data
self.update_element(
path=['data', 'data'],
action='set',
data=transformed_data
)
@property
def transformer(self):
"""
:return: The currently set transformer.
"""
return self._transformer
@transformer.setter
def transformer(self, value):
"""
Sets the transformer for the chart.
"""
self._transformer = self._get_transformer(value)
@staticmethod
def _get_transformer(transform):
transformer = transform or 'noop'
if isinstance(transformer, Transformer):
return transformer
elif isinstance(transformer, dict):
transformer_key = transformer.pop('type')
return transformer_classes[transformer_key](**transformer)
if isinstance(transformer, six.string_types):
if transformer in transformers:
return transformers[transformer]
maybe_dict_transformer = DictLevelsTransformer.from_str(transformer)
if maybe_dict_transformer:
transformers[maybe_dict_transformer.key] = maybe_dict_transformer
return maybe_dict_transformer
raise ValueError('No matching transformer found for {}'.format(transform))