From 1de96fcfb542ae456bd925cf9fe056bce106547d Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sat, 28 Jan 2017 11:58:43 +0000 Subject: [PATCH 01/14] Fixed bug unpacking Overlay and Layout objects in Layout.from_values --- holoviews/core/layout.py | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/holoviews/core/layout.py b/holoviews/core/layout.py index f994b72471..7b61dc566e 100644 --- a/holoviews/core/layout.py +++ b/holoviews/core/layout.py @@ -366,6 +366,28 @@ def relabel_item_paths(cls, items): return list(zip(paths, path_items)) + @classmethod + def _unpack_paths(cls, objs, paths, items, count=2): + """ + Recursively unpacks lists and Layout-like objects, accumulating + into the supplied list of items. + """ + if isinstance(objs, cls): + objs = objs.values() + for v in objs: + if isinstance(v, cls): + cls._unpack_paths(v, paths, items, count) + continue + group = group_sanitizer(v.group) + group = ''.join([group[0].upper(), group[1:]]) + label = label_sanitizer(v.label if v.label else 'I') + label = ''.join([label[0].upper(), label[1:]]) + new_path, count = cls.new_path((group, label), v, paths, count) + new_path = tuple(''.join((p[0].upper(), p[1:])) for p in new_path) + paths.append(new_path) + items.append((new_path, v)) + + @classmethod def from_values(cls, val): """ @@ -378,16 +400,7 @@ def from_values(cls, val): elif not collection: val = [val] paths, items = [], [] - count = 2 - for v in val: - group = group_sanitizer(v.group) - group = ''.join([group[0].upper(), group[1:]]) - label = label_sanitizer(v.label if v.label else 'I') - label = ''.join([label[0].upper(), label[1:]]) - new_path, count = cls.new_path((group, label), v, paths, count) - new_path = tuple(''.join((p[0].upper(), p[1:])) for p in new_path) - paths.append(new_path) - items.append((new_path, v)) + cls._unpack_paths(val, paths, items) return cls(items=items) From 032ba9a76a4b994f3bd7d26bb55b8c79ff94757f Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sat, 28 Jan 2017 12:21:31 +0000 Subject: [PATCH 02/14] Correctly generate counts on Layout and Overlay paths --- holoviews/core/layout.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/holoviews/core/layout.py b/holoviews/core/layout.py index 7b61dc566e..db3cf2ed9a 100644 --- a/holoviews/core/layout.py +++ b/holoviews/core/layout.py @@ -7,6 +7,7 @@ from functools import reduce from itertools import chain +from collections import defaultdict import numpy as np @@ -329,22 +330,23 @@ def collate(cls, data, kdims=None, key_dimensions=None): @classmethod - def new_path(cls, path, item, paths, count): + def new_path(cls, path, item, paths, counts): sanitizers = [sanitize_identifier, group_sanitizer, label_sanitizer] path = tuple(fn(p) for (p, fn) in zip(path, sanitizers)) while any(path[:i] in paths or path in [p[:i] for p in paths] for i in range(1,len(path)+1)): path = path[:2] pl = len(path) + count = counts[path[:-1]] if (pl == 1 and not item.label) or (pl == 2 and item.label): new_path = path + (int_to_roman(count-1),) if path in paths: paths[paths.index(path)] = new_path path = path + (int_to_roman(count),) else: - path = path[:-1] + (int_to_roman(count),) - count += 1 - return path, count + path = path[:-1] + (int_to_roman(count-1),) + counts[path[:-1]] += 1 + return path @classmethod @@ -357,9 +359,9 @@ def relabel_item_paths(cls, items): identifiers if necessary. """ paths, path_items = [], [] - count = 2 + counts = defaultdict(lambda: 2) for path, item in items: - new_path, count = cls.new_path(path, item, paths, count) + new_path = cls.new_path(path, item, paths, counts) new_path = tuple(''.join((p[0].upper(), p[1:])) for p in new_path) path_items.append(item) paths.append(new_path) @@ -367,7 +369,7 @@ def relabel_item_paths(cls, items): @classmethod - def _unpack_paths(cls, objs, paths, items, count=2): + def _unpack_paths(cls, objs, paths, items, counts): """ Recursively unpacks lists and Layout-like objects, accumulating into the supplied list of items. @@ -376,13 +378,13 @@ def _unpack_paths(cls, objs, paths, items, count=2): objs = objs.values() for v in objs: if isinstance(v, cls): - cls._unpack_paths(v, paths, items, count) + cls._unpack_paths(v, paths, items, counts) continue group = group_sanitizer(v.group) group = ''.join([group[0].upper(), group[1:]]) label = label_sanitizer(v.label if v.label else 'I') label = ''.join([label[0].upper(), label[1:]]) - new_path, count = cls.new_path((group, label), v, paths, count) + new_path = cls.new_path((group, label), v, paths, counts) new_path = tuple(''.join((p[0].upper(), p[1:])) for p in new_path) paths.append(new_path) items.append((new_path, v)) @@ -400,7 +402,8 @@ def from_values(cls, val): elif not collection: val = [val] paths, items = [], [] - cls._unpack_paths(val, paths, items) + counts = defaultdict(lambda: 2) + cls._unpack_paths(val, paths, items, counts) return cls(items=items) From 7133309e18f5934329091814310f4c1b466ede65 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sat, 28 Jan 2017 12:32:03 +0000 Subject: [PATCH 03/14] Small fix to Layout path count code --- holoviews/core/layout.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/holoviews/core/layout.py b/holoviews/core/layout.py index db3cf2ed9a..337907cacc 100644 --- a/holoviews/core/layout.py +++ b/holoviews/core/layout.py @@ -344,7 +344,7 @@ def new_path(cls, path, item, paths, counts): paths[paths.index(path)] = new_path path = path + (int_to_roman(count),) else: - path = path[:-1] + (int_to_roman(count-1),) + path = path[:-1] + (int_to_roman(count),) counts[path[:-1]] += 1 return path From 8d65c19d4dad5513c0063afb6e24a7e1a7360e97 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 10 Feb 2017 01:24:55 +0000 Subject: [PATCH 04/14] Fixes for Layout/Overlay path resolution --- holoviews/core/layout.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/holoviews/core/layout.py b/holoviews/core/layout.py index 337907cacc..bf6a789fd5 100644 --- a/holoviews/core/layout.py +++ b/holoviews/core/layout.py @@ -338,6 +338,7 @@ def new_path(cls, path, item, paths, counts): path = path[:2] pl = len(path) count = counts[path[:-1]] + counts[path[:-1]] += 1 if (pl == 1 and not item.label) or (pl == 2 and item.label): new_path = path + (int_to_roman(count-1),) if path in paths: @@ -345,7 +346,6 @@ def new_path(cls, path, item, paths, counts): path = path + (int_to_roman(count),) else: path = path[:-1] + (int_to_roman(count),) - counts[path[:-1]] += 1 return path @@ -374,10 +374,10 @@ def _unpack_paths(cls, objs, paths, items, counts): Recursively unpacks lists and Layout-like objects, accumulating into the supplied list of items. """ - if isinstance(objs, cls): + if type(objs) is cls: objs = objs.values() for v in objs: - if isinstance(v, cls): + if type(v) is cls: cls._unpack_paths(v, paths, items, counts) continue group = group_sanitizer(v.group) From fcf6e1ebcc3d188375f01fb40cb6754572388821 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sat, 11 Feb 2017 19:09:54 +0000 Subject: [PATCH 05/14] Simplified Layout/Overlay path resolution --- holoviews/core/layout.py | 57 +++++++++++++++++++-------------------- holoviews/core/overlay.py | 15 +++-------- 2 files changed, 30 insertions(+), 42 deletions(-) diff --git a/holoviews/core/layout.py b/holoviews/core/layout.py index bf6a789fd5..ed3bf12b61 100644 --- a/holoviews/core/layout.py +++ b/holoviews/core/layout.py @@ -7,7 +7,7 @@ from functools import reduce from itertools import chain -from collections import defaultdict +from collections import defaultdict, Counter import numpy as np @@ -333,41 +333,18 @@ def collate(cls, data, kdims=None, key_dimensions=None): def new_path(cls, path, item, paths, counts): sanitizers = [sanitize_identifier, group_sanitizer, label_sanitizer] path = tuple(fn(p) for (p, fn) in zip(path, sanitizers)) - while any(path[:i] in paths or path in [p[:i] for p in paths] - for i in range(1,len(path)+1)): + while path in paths: path = path[:2] pl = len(path) count = counts[path[:-1]] counts[path[:-1]] += 1 if (pl == 1 and not item.label) or (pl == 2 and item.label): - new_path = path + (int_to_roman(count-1),) - if path in paths: - paths[paths.index(path)] = new_path - path = path + (int_to_roman(count),) + path = path + (int_to_roman(count-1),) else: path = path[:-1] + (int_to_roman(count),) return path - @classmethod - def relabel_item_paths(cls, items): - """ - Given a list of path items (list of tuples where each element - is a (path, element) pair), generate a new set of path items that - guarantees that no paths clash. This uses the element labels as - appropriate and automatically generates roman numeral - identifiers if necessary. - """ - paths, path_items = [], [] - counts = defaultdict(lambda: 2) - for path, item in items: - new_path = cls.new_path(path, item, paths, counts) - new_path = tuple(''.join((p[0].upper(), p[1:])) for p in new_path) - path_items.append(item) - paths.append(new_path) - return list(zip(paths, path_items)) - - @classmethod def _unpack_paths(cls, objs, paths, items, counts): """ @@ -389,6 +366,25 @@ def _unpack_paths(cls, objs, paths, items, counts): paths.append(new_path) items.append((new_path, v)) + @classmethod + def _initial_paths(cls, vals, paths=None): + if paths is None: + paths = [] + + for v in vals: + if type(v) is cls: + cls._initial_paths(v.values(), paths) + continue + group = group_sanitizer(v.group) + group = ''.join([group[0].upper(), group[1:]]) + if v.label: + label = label_sanitizer(v.label) + label = ''.join([label[0].upper(), label[1:]]) + path = (group, label) + else: + path = (group,) + paths.append(path) + return paths @classmethod def from_values(cls, val): @@ -401,7 +397,10 @@ def from_values(cls, val): return val elif not collection: val = [val] - paths, items = [], [] + paths = cls._initial_paths(val) + path_counter = Counter(paths) + paths = [k for k, v in path_counter.items() if v > 1] + items = [] counts = defaultdict(lambda: 2) cls._unpack_paths(val, paths, items, counts) return cls(items=items) @@ -518,9 +517,7 @@ def __len__(self): def __add__(self, other): - other = self.from_values(other) - items = list(self.data.items()) + list(other.data.items()) - return Layout(items=self.relabel_item_paths(items)).display('all') + return Layout.from_values([self, other]).display('all') diff --git a/holoviews/core/overlay.py b/holoviews/core/overlay.py index 6cd903d4b8..a2ba5075a6 100644 --- a/holoviews/core/overlay.py +++ b/holoviews/core/overlay.py @@ -36,10 +36,7 @@ def dynamic_mul(*args, **kwargs): items = [(k, self * v) for (k, v) in other.items()] return other.clone(items) - self_item = [((self.group, self.label if self.label else 'I'), self)] - other_items = (other.items() if isinstance(other, Overlay) - else [((other.group, other.label if other.label else 'I'), other)]) - return Overlay(items=Overlay.relabel_item_paths(list(self_item) + list(other_items))) + return Overlay.from_values([self, other]) @@ -142,15 +139,9 @@ def __add__(self, other): def __mul__(self, other): - if isinstance(other, Overlay): - items = list(self.data.items()) + list(other.data.items()) - elif isinstance(other, ViewableElement): - label = other.label if other.label else 'I' - items = list(self.data.items()) + [((other.group, label), other)] - elif isinstance(other, UniformNdMapping): + if isinstance(other, UniformNdMapping): raise NotImplementedError - - return Overlay(items=self.relabel_item_paths(items)).display('all') + return Overlay.from_values([self, other]) def collate(self): From de19047fa7eff38740b38943435997daebc3433a Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sat, 11 Feb 2017 19:39:36 +0000 Subject: [PATCH 06/14] Fixes for Overlay composition --- holoviews/core/overlay.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/holoviews/core/overlay.py b/holoviews/core/overlay.py index a2ba5075a6..9e190cd090 100644 --- a/holoviews/core/overlay.py +++ b/holoviews/core/overlay.py @@ -100,11 +100,6 @@ class Overlay(Layout, CompositeOverlay): Layout and CompositeOverlay. """ - @classmethod - def _from_values(cls, val): - return reduce(lambda x,y: x*y, val).map(lambda x: x.display('auto'), [Overlay]) - - def __init__(self, items=None, group=None, label=None, **params): view_params = ViewableElement.params().keys() self.__dict__['_fixed'] = False @@ -135,11 +130,11 @@ def get(self, identifier, default=None): def __add__(self, other): - return Layout.from_values(self) + Layout.from_values(other) + return Layout.from_values([self, other]) def __mul__(self, other): - if isinstance(other, UniformNdMapping): + if not isinstance(other, ViewableElement): raise NotImplementedError return Overlay.from_values([self, other]) From a6f6f2947d4ff550a0963e7c372c3ede47a9a8cd Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sat, 11 Feb 2017 23:41:25 +0000 Subject: [PATCH 07/14] Simplified path resolution on Layout --- holoviews/core/layout.py | 64 +++++++++++++++++++--------------------- 1 file changed, 30 insertions(+), 34 deletions(-) diff --git a/holoviews/core/layout.py b/holoviews/core/layout.py index ed3bf12b61..3abfe33f6e 100644 --- a/holoviews/core/layout.py +++ b/holoviews/core/layout.py @@ -331,17 +331,17 @@ def collate(cls, data, kdims=None, key_dimensions=None): @classmethod def new_path(cls, path, item, paths, counts): - sanitizers = [sanitize_identifier, group_sanitizer, label_sanitizer] - path = tuple(fn(p) for (p, fn) in zip(path, sanitizers)) + sanitizers = [group_sanitizer, label_sanitizer] + capitalize = lambda x: x[0].upper() + x[1:] + path = tuple(capitalize(fn(p)) for (p, fn) in zip(path, sanitizers)) while path in paths: path = path[:2] pl = len(path) count = counts[path[:-1]] counts[path[:-1]] += 1 - if (pl == 1 and not item.label) or (pl == 2 and item.label): - path = path + (int_to_roman(count-1),) - else: - path = path[:-1] + (int_to_roman(count),) + path = path + (int_to_roman(count),) + if len(path) == 1: + path = path + (int_to_roman(counts.get(path, 1)),) return path @@ -357,60 +357,56 @@ def _unpack_paths(cls, objs, paths, items, counts): if type(v) is cls: cls._unpack_paths(v, paths, items, counts) continue - group = group_sanitizer(v.group) - group = ''.join([group[0].upper(), group[1:]]) - label = label_sanitizer(v.label if v.label else 'I') - label = ''.join([label[0].upper(), label[1:]]) - new_path = cls.new_path((group, label), v, paths, counts) - new_path = tuple(''.join((p[0].upper(), p[1:])) for p in new_path) + path = (v.group, v.label) if v.label else (v.group,) + new_path = cls.new_path(path, v, paths, counts) paths.append(new_path) items.append((new_path, v)) + @classmethod def _initial_paths(cls, vals, paths=None): if paths is None: paths = [] - + capitalize = lambda x: x[0].upper() + x[1:] for v in vals: if type(v) is cls: cls._initial_paths(v.values(), paths) continue - group = group_sanitizer(v.group) - group = ''.join([group[0].upper(), group[1:]]) + path = (capitalize(group_sanitizer(v.group)),) if v.label: - label = label_sanitizer(v.label) - label = ''.join([label[0].upper(), label[1:]]) - path = (group, label) - else: - path = (group,) + path = path + (capitalize(label_sanitizer(v.label)),) paths.append(path) return paths + @classmethod - def from_values(cls, val): + def _process_items(cls, vals): + if type(vals) is cls: + return vals + elif not isinstance(vals, (list, tuple)): + vals = [vals] + paths = cls._initial_paths(vals) + path_counter = Counter(paths) + paths, items = [k for k, v in path_counter.items() if v > 1], [] + counts = defaultdict(lambda: 1) + cls._unpack_paths(vals, paths, items, counts) + return items + + + @classmethod + def from_values(cls, vals): """ Returns a Layout given a list (or tuple) of viewable elements or just a single viewable element. """ - collection = isinstance(val, (list, tuple)) - if type(val) is cls: - return val - elif not collection: - val = [val] - paths = cls._initial_paths(val) - path_counter = Counter(paths) - paths = [k for k, v in path_counter.items() if v > 1] - items = [] - counts = defaultdict(lambda: 2) - cls._unpack_paths(val, paths, items, counts) - return cls(items=items) + return cls(items=cls._process_items(vals)) def __init__(self, items=None, identifier=None, parent=None, **kwargs): self.__dict__['_display'] = 'auto' self.__dict__['_max_cols'] = 4 if items and all(isinstance(item, Dimensioned) for item in items): - items = self.from_values(items).data + items = self._process_items(items) params = {p: kwargs.pop(p) for p in list(self.params().keys())+['id'] if p in kwargs} AttrTree.__init__(self, items, identifier, parent, **kwargs) Dimensioned.__init__(self, self.data, **params) From aa20b33377f2eed8ee6b9169a9f97a2b4ac8fd46 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sun, 12 Feb 2017 01:58:46 +0000 Subject: [PATCH 08/14] Simplified __add__ methods --- holoviews/core/layout.py | 6 +++--- holoviews/core/spaces.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/holoviews/core/layout.py b/holoviews/core/layout.py index 3abfe33f6e..983e48920b 100644 --- a/holoviews/core/layout.py +++ b/holoviews/core/layout.py @@ -28,7 +28,7 @@ class Composable(object): """ def __add__(self, obj): - return Layout.from_values(self) + Layout.from_values(obj) + return Layout.from_values([self, obj]) def __lshift__(self, other): @@ -213,7 +213,7 @@ def __iter__(self): def __add__(self, obj): - return Layout.from_values(self) + Layout.from_values(obj) + return Layout.from_values([self, obj]) def __len__(self): @@ -269,7 +269,7 @@ def cols(self, n): def __add__(self, obj): - return Layout.from_values(self) + Layout.from_values(obj) + return Layout.from_values([self, obj]) @property diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py index 58b50a6d5b..7d856cbd5e 100644 --- a/holoviews/core/spaces.py +++ b/holoviews/core/spaces.py @@ -223,7 +223,7 @@ def dynamic_mul(*args, **kwargs): def __add__(self, obj): - return Layout.from_values(self) + Layout.from_values(obj) + return Layout.from_values([self, obj]) def __lshift__(self, other): @@ -1137,7 +1137,7 @@ def __len__(self): def __add__(self, obj): - return Layout.from_values(self) + Layout.from_values(obj) + return Layout.from_values([self, obj]) @property From b2365ac26eb3fbde5d43d2dccfd30b93451bf824 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sun, 12 Feb 2017 03:00:23 +0000 Subject: [PATCH 09/14] Further refactoring of Layouth path processing --- holoviews/core/layout.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/holoviews/core/layout.py b/holoviews/core/layout.py index 983e48920b..7a3c1a134c 100644 --- a/holoviews/core/layout.py +++ b/holoviews/core/layout.py @@ -330,15 +330,19 @@ def collate(cls, data, kdims=None, key_dimensions=None): @classmethod - def new_path(cls, path, item, paths, counts): + def _get_path(cls, item): + path = (item.group, item.label) if item.label else (item.group,) sanitizers = [group_sanitizer, label_sanitizer] capitalize = lambda x: x[0].upper() + x[1:] - path = tuple(capitalize(fn(p)) for (p, fn) in zip(path, sanitizers)) + return tuple(capitalize(fn(p)) for (p, fn) in zip(path, sanitizers)) + + + @classmethod + def new_path(cls, item, paths, counts): + path = cls._get_path(item) while path in paths: - path = path[:2] - pl = len(path) - count = counts[path[:-1]] - counts[path[:-1]] += 1 + count = counts[path] + counts[path] += 1 path = path + (int_to_roman(count),) if len(path) == 1: path = path + (int_to_roman(counts.get(path, 1)),) @@ -357,8 +361,7 @@ def _unpack_paths(cls, objs, paths, items, counts): if type(v) is cls: cls._unpack_paths(v, paths, items, counts) continue - path = (v.group, v.label) if v.label else (v.group,) - new_path = cls.new_path(path, v, paths, counts) + new_path = cls.new_path(v, paths, counts) paths.append(new_path) items.append((new_path, v)) @@ -367,22 +370,18 @@ def _unpack_paths(cls, objs, paths, items, counts): def _initial_paths(cls, vals, paths=None): if paths is None: paths = [] - capitalize = lambda x: x[0].upper() + x[1:] for v in vals: if type(v) is cls: cls._initial_paths(v.values(), paths) continue - path = (capitalize(group_sanitizer(v.group)),) - if v.label: - path = path + (capitalize(label_sanitizer(v.label)),) - paths.append(path) + paths.append(cls._get_path(v)) return paths @classmethod def _process_items(cls, vals): if type(vals) is cls: - return vals + return vals.data elif not isinstance(vals, (list, tuple)): vals = [vals] paths = cls._initial_paths(vals) From 2e024e1f2cd64bf0e506724da489188c173a9869 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 13 Feb 2017 00:23:29 +0000 Subject: [PATCH 10/14] Handle inheriting old paths in Layout path resolution --- holoviews/core/layout.py | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/holoviews/core/layout.py b/holoviews/core/layout.py index 7a3c1a134c..d88efcdc2a 100644 --- a/holoviews/core/layout.py +++ b/holoviews/core/layout.py @@ -331,15 +331,19 @@ def collate(cls, data, kdims=None, key_dimensions=None): @classmethod def _get_path(cls, item): - path = (item.group, item.label) if item.label else (item.group,) sanitizers = [group_sanitizer, label_sanitizer] capitalize = lambda x: x[0].upper() + x[1:] + if isinstance(item, tuple): + path, item = item + new_path = cls._get_path(item) + path = path[:2] if item.label and path[1] == new_path[1] else path[:1] + else: + path = (item.group, item.label) if item.label else (item.group,) return tuple(capitalize(fn(p)) for (p, fn) in zip(path, sanitizers)) @classmethod - def new_path(cls, item, paths, counts): - path = cls._get_path(item) + def new_path(cls, path, paths, counts): while path in paths: count = counts[path] counts[path] += 1 @@ -356,25 +360,28 @@ def _unpack_paths(cls, objs, paths, items, counts): into the supplied list of items. """ if type(objs) is cls: - objs = objs.values() - for v in objs: - if type(v) is cls: - cls._unpack_paths(v, paths, items, counts) + objs = objs.items() + for item in objs: + path, obj = item if isinstance(item, tuple) else (None, item) + if type(obj) is cls: + cls._unpack_paths(obj, paths, items, counts) continue - new_path = cls.new_path(v, paths, counts) + path = cls._get_path(item) + new_path = cls.new_path(path, paths, counts) paths.append(new_path) - items.append((new_path, v)) + items.append((new_path, obj)) @classmethod - def _initial_paths(cls, vals, paths=None): + def _initial_paths(cls, items, paths=None): if paths is None: paths = [] - for v in vals: - if type(v) is cls: - cls._initial_paths(v.values(), paths) + for item in items: + path, item = item if isinstance(item, tuple) else (None, item) + if type(item) is cls: + cls._initial_paths(item.items(), paths) continue - paths.append(cls._get_path(v)) + paths.append(cls._get_path(item)) return paths From 8ff53b095145a10bac817ff068a0cef08d4c96a0 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 14 Feb 2017 16:03:39 +0000 Subject: [PATCH 11/14] Factored Layout/Overlay path finding code out into utilities --- holoviews/core/layout.py | 52 ++++++++++++++-------------------------- holoviews/core/util.py | 37 +++++++++++++++++++++++++++- 2 files changed, 54 insertions(+), 35 deletions(-) diff --git a/holoviews/core/layout.py b/holoviews/core/layout.py index d88efcdc2a..568f3774f8 100644 --- a/holoviews/core/layout.py +++ b/holoviews/core/layout.py @@ -16,8 +16,7 @@ from .dimension import Dimension, Dimensioned, ViewableElement from .ndmapping import OrderedDict, NdMapping, UniformNdMapping from .tree import AttrTree -from .util import (int_to_roman, sanitize_identifier, group_sanitizer, - label_sanitizer, unique_array) +from .util import (unique_array, get_path, make_path_unique) from . import traversal @@ -330,31 +329,7 @@ def collate(cls, data, kdims=None, key_dimensions=None): @classmethod - def _get_path(cls, item): - sanitizers = [group_sanitizer, label_sanitizer] - capitalize = lambda x: x[0].upper() + x[1:] - if isinstance(item, tuple): - path, item = item - new_path = cls._get_path(item) - path = path[:2] if item.label and path[1] == new_path[1] else path[:1] - else: - path = (item.group, item.label) if item.label else (item.group,) - return tuple(capitalize(fn(p)) for (p, fn) in zip(path, sanitizers)) - - - @classmethod - def new_path(cls, path, paths, counts): - while path in paths: - count = counts[path] - counts[path] += 1 - path = path + (int_to_roman(count),) - if len(path) == 1: - path = path + (int_to_roman(counts.get(path, 1)),) - return path - - - @classmethod - def _unpack_paths(cls, objs, paths, items, counts): + def _unpack_paths(cls, objs, items, counts): """ Recursively unpacks lists and Layout-like objects, accumulating into the supplied list of items. @@ -364,16 +339,19 @@ def _unpack_paths(cls, objs, paths, items, counts): for item in objs: path, obj = item if isinstance(item, tuple) else (None, item) if type(obj) is cls: - cls._unpack_paths(obj, paths, items, counts) + cls._unpack_paths(obj, items, counts) continue - path = cls._get_path(item) - new_path = cls.new_path(path, paths, counts) - paths.append(new_path) + path = get_path(item) + new_path = make_path_unique(path, counts) items.append((new_path, obj)) @classmethod def _initial_paths(cls, items, paths=None): + """ + Recurses the passed items finding paths for each. Useful for + determining which paths are not unique and have to be resolved. + """ if paths is None: paths = [] for item in items: @@ -381,21 +359,27 @@ def _initial_paths(cls, items, paths=None): if type(item) is cls: cls._initial_paths(item.items(), paths) continue - paths.append(cls._get_path(item)) + paths.append(get_path(item)) return paths @classmethod def _process_items(cls, vals): + """ + Processes a list of Labelled types unpacking any objects of + the same type (e.g. a Layout) and finding unique paths for + all the items in the list. + """ if type(vals) is cls: return vals.data elif not isinstance(vals, (list, tuple)): vals = [vals] paths = cls._initial_paths(vals) path_counter = Counter(paths) - paths, items = [k for k, v in path_counter.items() if v > 1], [] + items = [] counts = defaultdict(lambda: 1) - cls._unpack_paths(vals, paths, items, counts) + counts.update({k: 1 for k, v in path_counter.items() if v > 1}) + cls._unpack_paths(vals, items, counts) return items diff --git a/holoviews/core/util.py b/holoviews/core/util.py index 8011bdc877..7e3f7ec1e3 100644 --- a/holoviews/core/util.py +++ b/holoviews/core/util.py @@ -4,7 +4,7 @@ import string, fnmatch import unicodedata import datetime as dt -from collections import defaultdict +from collections import defaultdict, Counter import numpy as np import param @@ -1002,6 +1002,41 @@ def unpack_group(group, getter): yield (wrap_tuple(key), obj) +def capitalize(string): + """ + Capitalizes the first letter of a string. + """ + return string[0].upper() + string[1:] + + +def get_path(item): + """ + Gets a path from an Labelled object or from a tuple of an existing + path and a labelled object. The path strings are sanitized and + capitalized. + """ + sanitizers = [group_sanitizer, label_sanitizer] + if isinstance(item, tuple): + path, item = item + new_path = get_path(item) + path = path[:2] if item.label and path[1] == new_path[1] else path[:1] + else: + path = (item.group, item.label) if item.label else (item.group,) + return tuple(capitalize(fn(p)) for (p, fn) in zip(path, sanitizers)) + + +def make_path_unique(path, counts): + """ + Given a path, a list of existing paths and counts for each of the + existing paths. + """ + while path in counts: + count = counts[path] + counts[path] += 1 + path = path + (int_to_roman(count),) + if len(path) == 1: + path = path + (int_to_roman(counts.get(path, 1)),) + return path class ndmapping_groupby(param.ParameterizedFunction): From 4b34f0b17340ed9d01132a832cfa40699cbad8ed Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 14 Feb 2017 20:25:16 +0000 Subject: [PATCH 12/14] Reordered Layout methods --- holoviews/core/layout.py | 84 ++++++++++++++++++++-------------------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/holoviews/core/layout.py b/holoviews/core/layout.py index 568f3774f8..b433062e2f 100644 --- a/holoviews/core/layout.py +++ b/holoviews/core/layout.py @@ -314,6 +314,16 @@ class Layout(AttrTree, Dimensioned): _deep_indexable = True + def __init__(self, items=None, identifier=None, parent=None, **kwargs): + self.__dict__['_display'] = 'auto' + self.__dict__['_max_cols'] = 4 + if items and all(isinstance(item, Dimensioned) for item in items): + items = self._process_items(items) + params = {p: kwargs.pop(p) for p in list(self.params().keys())+['id'] if p in kwargs} + AttrTree.__init__(self, items, identifier, parent, **kwargs) + Dimensioned.__init__(self, self.data, **params) + + @classmethod def collate(cls, data, kdims=None, key_dimensions=None): kdims = key_dimensions if (kdims is None) else kdims @@ -329,38 +339,12 @@ def collate(cls, data, kdims=None, key_dimensions=None): @classmethod - def _unpack_paths(cls, objs, items, counts): - """ - Recursively unpacks lists and Layout-like objects, accumulating - into the supplied list of items. - """ - if type(objs) is cls: - objs = objs.items() - for item in objs: - path, obj = item if isinstance(item, tuple) else (None, item) - if type(obj) is cls: - cls._unpack_paths(obj, items, counts) - continue - path = get_path(item) - new_path = make_path_unique(path, counts) - items.append((new_path, obj)) - - - @classmethod - def _initial_paths(cls, items, paths=None): + def from_values(cls, vals): """ - Recurses the passed items finding paths for each. Useful for - determining which paths are not unique and have to be resolved. + Returns a Layout given a list (or tuple) of viewable + elements or just a single viewable element. """ - if paths is None: - paths = [] - for item in items: - path, item = item if isinstance(item, tuple) else (None, item) - if type(item) is cls: - cls._initial_paths(item.items(), paths) - continue - paths.append(get_path(item)) - return paths + return cls(items=cls._process_items(vals)) @classmethod @@ -384,22 +368,38 @@ def _process_items(cls, vals): @classmethod - def from_values(cls, vals): + def _initial_paths(cls, items, paths=None): """ - Returns a Layout given a list (or tuple) of viewable - elements or just a single viewable element. + Recurses the passed items finding paths for each. Useful for + determining which paths are not unique and have to be resolved. """ - return cls(items=cls._process_items(vals)) + if paths is None: + paths = [] + for item in items: + path, item = item if isinstance(item, tuple) else (None, item) + if type(item) is cls: + cls._initial_paths(item.items(), paths) + continue + paths.append(get_path(item)) + return paths - def __init__(self, items=None, identifier=None, parent=None, **kwargs): - self.__dict__['_display'] = 'auto' - self.__dict__['_max_cols'] = 4 - if items and all(isinstance(item, Dimensioned) for item in items): - items = self._process_items(items) - params = {p: kwargs.pop(p) for p in list(self.params().keys())+['id'] if p in kwargs} - AttrTree.__init__(self, items, identifier, parent, **kwargs) - Dimensioned.__init__(self, self.data, **params) + @classmethod + def _unpack_paths(cls, objs, items, counts): + """ + Recursively unpacks lists and Layout-like objects, accumulating + into the supplied list of items. + """ + if type(objs) is cls: + objs = objs.items() + for item in objs: + path, obj = item if isinstance(item, tuple) else (None, item) + if type(obj) is cls: + cls._unpack_paths(obj, items, counts) + continue + path = get_path(item) + new_path = make_path_unique(path, counts) + items.append((new_path, obj)) @property From e63d161093a6e17c56825064e843f6bb3f0f5712 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 23 Feb 2017 13:41:36 +0000 Subject: [PATCH 13/14] Small fix for get_path utility --- holoviews/core/util.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/holoviews/core/util.py b/holoviews/core/util.py index 7e3f7ec1e3..e775fb715e 100644 --- a/holoviews/core/util.py +++ b/holoviews/core/util.py @@ -1018,8 +1018,13 @@ def get_path(item): sanitizers = [group_sanitizer, label_sanitizer] if isinstance(item, tuple): path, item = item - new_path = get_path(item) - path = path[:2] if item.label and path[1] == new_path[1] else path[:1] + if item.label: + if len(path) > 1 and item.label == path[1]: + path = path[:2] + else: + path = path[:1] + (item.label,) + else: + path = path[:1] else: path = (item.group, item.label) if item.label else (item.group,) return tuple(capitalize(fn(p)) for (p, fn) in zip(path, sanitizers)) From 6bf5d4e3c3a93b262ec15dde7f6b2ef6cdd955e7 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 23 Feb 2017 13:43:20 +0000 Subject: [PATCH 14/14] Added unit tests to test path resolution on Layout/Overlay --- tests/testcomposites.py | 50 ++++++++++++++++++++++++++++++++++++++++ tests/testutils.py | 51 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 99 insertions(+), 2 deletions(-) diff --git a/tests/testcomposites.py b/tests/testcomposites.py index 3774a4adb8..7c405c5dbb 100644 --- a/tests/testcomposites.py +++ b/tests/testcomposites.py @@ -126,6 +126,31 @@ def test_layouttree_quadruple_2(self): ('Element', 'LabelA', 'III'), ('Element', 'LabelA', 'IV')]) + def test_layout_from_values_with_layouts(self): + layout1 = self.el1 + self.el4 + layout2 = self.el2 + self.el5 + paths = Layout.from_values([layout1, layout2]).keys() + self.assertEqual(paths, [('Element', 'I'), ('ValA', 'I'), + ('Element', 'II'), ('ValB', 'I')]) + + def test_layout_from_values_with_mixed_types(self): + layout1 = self.el1 + self.el4 + self.el7 + layout2 = self.el2 + self.el5 + self.el8 + paths = Layout.from_values([layout1, layout2, self.el3]).keys() + self.assertEqual(paths, [('Element', 'I'), ('ValA', 'I'), + ('ValA', 'LabelA'), ('Element', 'II'), + ('ValB', 'I'), ('ValA', 'LabelB'), + ('Element', 'III')]) + + def test_layout_from_values_retains_custom_path(self): + layout = Layout([('Custom', self.el1)]) + paths = Layout.from_values([layout, self.el2]).keys() + self.assertEqual(paths, [('Custom', 'I'), ('Element', 'I')]) + + def test_layout_from_values_retains_custom_path_with_label(self): + layout = Layout([('Custom', self.el6)]) + paths = Layout.from_values([layout, self.el2]).keys() + self.assertEqual(paths, [('Custom', 'LabelA'), ('Element', 'I')]) class OverlayTestCase(ElementTestCase): @@ -253,6 +278,31 @@ def test_overlay_quadruple_2(self): ('Element', 'LabelA', 'III'), ('Element', 'LabelA', 'IV')]) + def test_overlay_from_values_with_layouts(self): + layout1 = self.el1 + self.el4 + layout2 = self.el2 + self.el5 + paths = Layout.from_values([layout1, layout2]).keys() + self.assertEqual(paths, [('Element', 'I'), ('ValA', 'I'), + ('Element', 'II'), ('ValB', 'I')]) + + def test_overlay_from_values_with_mixed_types(self): + overlay1 = self.el1 + self.el4 + self.el7 + overlay2 = self.el2 + self.el5 + self.el8 + paths = Layout.from_values([overlay1, overlay2, self.el3]).keys() + self.assertEqual(paths, [('Element', 'I'), ('ValA', 'I'), + ('ValA', 'LabelA'), ('Element', 'II'), + ('ValB', 'I'), ('ValA', 'LabelB'), + ('Element', 'III')]) + + def test_overlay_from_values_retains_custom_path(self): + overlay = Overlay([('Custom', self.el1)]) + paths = Overlay.from_values([overlay, self.el2]).keys() + self.assertEqual(paths, [('Custom', 'I'), ('Element', 'I')]) + + def test_overlay_from_values_retains_custom_path_with_label(self): + overlay = Overlay([('Custom', self.el6)]) + paths = Overlay.from_values([overlay, self.el2]).keys() + self.assertEqual(paths, [('Custom', 'LabelA'), ('Element', 'I')]) diff --git a/tests/testutils.py b/tests/testutils.py index 5aabb9954c..ca122b050e 100644 --- a/tests/testutils.py +++ b/tests/testutils.py @@ -16,9 +16,9 @@ from holoviews.core.util import ( sanitize_identifier_fn, find_range, max_range, wrap_tuple_streams, - deephash, merge_dimensions + deephash, merge_dimensions, get_path, make_path_unique ) -from holoviews import Dimension +from holoviews import Dimension, Element from holoviews.streams import PositionXY from holoviews.element.comparison import ComparisonTestCase @@ -463,3 +463,50 @@ def test_merge_dimensions_with_values(self): [Dimension('A', values=[1, 2]), Dimension('B')]]) self.assertEqual(dimensions, [Dimension('A'), Dimension('B')]) self.assertEqual(dimensions[0].values, [0, 1, 2]) + + +class TestTreePathUtils(unittest.TestCase): + + def test_get_path_with_label(self): + path = get_path(Element('Test', label='A')) + self.assertEqual(path, ('Element', 'A')) + + def test_get_path_without_label(self): + path = get_path(Element('Test')) + self.assertEqual(path, ('Element',)) + + def test_get_path_with_custom_group(self): + path = get_path(Element('Test', group='Custom Group')) + self.assertEqual(path, ('Custom_Group',)) + + def test_get_path_with_custom_group_and_label(self): + path = get_path(Element('Test', group='Custom Group', label='A')) + self.assertEqual(path, ('Custom_Group', 'A')) + + def test_get_path_from_item_with_custom_group(self): + path = get_path((('Custom',), Element('Test'))) + self.assertEqual(path, ('Custom',)) + + def test_get_path_from_item_with_custom_group_and_label(self): + path = get_path((('Custom', 'Path'), Element('Test'))) + self.assertEqual(path, ('Custom',)) + + def test_get_path_from_item_with_custom_group_and_matching_label(self): + path = get_path((('Custom', 'Path'), Element('Test', label='Path'))) + self.assertEqual(path, ('Custom', 'Path')) + + def test_make_path_unique_no_clash(self): + path = ('Element', 'A') + new_path = make_path_unique(path, {}) + self.assertEqual(new_path, path) + + def test_make_path_unique_clash_without_label(self): + path = ('Element',) + new_path = make_path_unique(path, {path: 1}) + self.assertEqual(new_path, path+('I',)) + + def test_make_path_unique_clash_with_label(self): + path = ('Element', 'A') + new_path = make_path_unique(path, {path: 1}) + self.assertEqual(new_path, path+('I',)) +