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',))
+