Skip to content

Commit

Permalink
Add fancy_layout option to HoloViews pane (#543)
Browse files Browse the repository at this point in the history
  • Loading branch information
philippjfr committed Oct 3, 2019
1 parent 80a6dd1 commit c75d6ec
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 12 deletions.
52 changes: 41 additions & 11 deletions panel/pane/holoviews.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@
from functools import partial

import param

from bokeh.models import Spacer as _BkSpacer

from ..io import state
from ..layout import Panel, Column
from ..layout import Panel, Column, WidgetBox, HSpacer, VSpacer
from ..viewable import Viewable
from ..widgets import Player
from .base import PaneBase, Pane
Expand All @@ -32,6 +33,10 @@ class HoloViews(PaneBase):
The HoloViews backend used to render the plot (if None defaults
to the currently selected renderer).""")

fancy_layout = param.Boolean(default=False, constant=True, doc="""
Whether the widgets should be laid out like the classic HoloViews
widgets.""")

linked_axes = param.Boolean(default=True, doc="""
Whether to use link the axes of bokeh plots inside this pane
across a panel layout.""")
Expand Down Expand Up @@ -59,8 +64,12 @@ class HoloViews(PaneBase):

def __init__(self, object=None, **params):
super(HoloViews, self).__init__(object, **params)
self.widget_box = Column()
self.widget_box = WidgetBox() if self.fancy_layout else Column()
if self.fancy_layout:
self.layout.insert(0, HSpacer())
self._update_widgets()
if self.fancy_layout:
self.layout.insert(2, HSpacer())
self._plots = {}
self.param.watch(self._update_widgets, self._rerender_params)

Expand All @@ -72,8 +81,8 @@ def _update_widgets(self, *events):
if self.object is None:
widgets, values = [], []
else:
widgets, values = self.widgets_from_dimensions(self.object, self.widgets,
self.widget_type)
widgets, values = self.widgets_from_dimensions(
self.object, self.widgets, self.widget_type, fancy=self.fancy_layout)
self._values = values

# Clean up anything models listening to the previous widgets
Expand All @@ -89,9 +98,15 @@ def _update_widgets(self, *events):

self.widget_box.objects = widgets
if widgets and not self.widget_box in self.layout.objects:
self.layout.append(self.widget_box)
elif not widgets and self.widget_box in self.layout.objects:
self.layout.pop(self.widget_box)
if self.fancy_layout:
self.layout.append(Column(VSpacer(), self.widget_box, VSpacer()))
else:
self.layout.append(self.widget_box)
elif not widgets:
if self.fancy_layout and self.widget_box in self.layout[-1]:
self.layout.pop(-1)
elif self.widget_box in self.layout.objects:
self.layout.pop(self.widget_box)

def _update_plot(self, plot, pane):
from holoviews.core.util import cross_index, wrap_tuple_streams
Expand Down Expand Up @@ -196,8 +211,8 @@ def applies(cls, obj):
from holoviews.plotting.plot import Plot
return isinstance(obj, Dimensioned) or isinstance(obj, Plot)

@classmethod
def widgets_from_dimensions(cls, object, widget_types={}, widgets_type='individual'):
def widgets_from_dimensions(cls, object, widget_types={}, widgets_type='individual',
fancy=False):
from holoviews.core import Dimension, DynamicMap
from holoviews.core.options import SkipRendering
from holoviews.core.util import isnumeric, unicode, datetime_types, unique_iterator
Expand Down Expand Up @@ -228,8 +243,21 @@ def widgets_from_dimensions(cls, object, widget_types={}, widgets_type='individu
values = dict() if dynamic else dict(zip(dims, zip(*keys)))
dim_values = OrderedDict()
widgets = []
for dim in dims:
for i, dim in enumerate(dims):
widget_type, widget, widget_kwargs = None, None, {}
if fancy:
if i == 0 and i == (len(dims)-1):
margin = (20, 20, 20, 20)
elif i == 0:
margin = (20, 20, 5, 20)
elif i == (len(dims)-1):
margin = (5, 20, 20, 20)
else:
margin = (0, 20, 5, 20)
kwargs = {'margin': margin, 'width': 250}
else:
kwargs = {}

vals = dim.values or values.get(dim, None)
if vals is not None:
vals = list(unique_iterator(vals))
Expand All @@ -245,14 +273,16 @@ def widgets_from_dimensions(cls, object, widget_types={}, widgets_type='individu
continue
elif isinstance(widget, dict):
widget_type = widget.get('type', widget_type)
widget_kwargs = widget
widget_kwargs = dict(widget)
elif isinstance(widget, type) and issubclass(widget, Widget):
widget_type = widget
else:
raise ValueError('Explicit widget definitions expected '
'to be a widget instance or type, %s '
'dimension widget declared as %s.' %
(dim, widget))
widget_kwargs.update(kwargs)

if vals:
if all(isnumeric(v) or isinstance(v, datetime_types) for v in vals) and len(vals) > 1:
vals = sorted(vals)
Expand Down
25 changes: 25 additions & 0 deletions panel/tests/pane/test_holoviews.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,31 @@ def test_holoviews_with_widgets_not_shown(document, comm):
assert hv_pane.widget_box.objects[1].name == 'B'


@hv_available
def test_holoviews_fancy_layout(document, comm):
hmap = hv.HoloMap({(i, chr(65+i)): hv.Curve([i]) for i in range(3)}, kdims=['X', 'Y'])

hv_pane = HoloViews(hmap, fancy_layout=True)
layout_obj = hv_pane.layout
layout = layout_obj.get_root(document, comm)
model = layout.children[1]
assert hv_pane is layout_obj[1]
assert len(hv_pane.widget_box.objects) == 2
assert hv_pane.widget_box is layout_obj[-1][1]
assert hv_pane.widget_box.objects[0].name == 'X'
assert hv_pane.widget_box.objects[1].name == 'Y'

assert hv_pane._models[layout.ref['id']][1].children[1] is model

hv_pane.object = hv.Curve([1, 2, 3])
assert len(hv_pane.widget_box.objects) == 0
assert len(layout_obj) == 3
assert hv_pane is layout_obj[1]

hv_pane.object = hmap
assert hv_pane.widget_box is layout_obj[-1][1]


@hv_available
def test_holoviews_widgets_from_holomap():
hmap = hv.HoloMap({(i, chr(65+i)): hv.Curve([i]) for i in range(3)}, kdims=['X', 'Y'])
Expand Down
23 changes: 22 additions & 1 deletion panel/widgets/slider.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from ..config import config
from ..io import state
from ..util import value_as_datetime, value_as_date
from ..viewable import Layoutable
from .base import Widget, CompositeWidget
from ..layout import Column
from .input import StaticText
Expand Down Expand Up @@ -168,11 +169,12 @@ def __init__(self, **params):
% self.value)

self._text = StaticText(margin=(5, 0, 0, 5), style={'white-space': 'nowrap'})
self._slider = IntSlider()
self._slider = None
self._composite = Column(self._text, self._slider)
self._update_options()
self.param.watch(self._update_options, ['options', 'formatter'])
self.param.watch(self._update_value, ['value'])
self.param.watch(self._update_style, [p for p in Layoutable.param if p !='name'])

def _update_options(self, *events):
values, labels = self.values, self.labels
Expand All @@ -181,9 +183,11 @@ def _update_options(self, *events):
self.value = values[0]
else:
value = values.index(self.value)

self._slider = IntSlider(start=0, end=len(self.options)-1, value=value,
tooltips=False, show_value=False, margin=(0, 5, 5, 5),
_supports_embed=False)
self._update_style()
js_code = self._text_link.format(labels=repr(self.labels))
self._jslink = self._slider.jslink(self._text, code={'value': js_code})
self._slider.param.watch(self._sync_value, 'value')
Expand All @@ -204,6 +208,23 @@ def _update_value(self, event):
finally:
self._syncing = False

def _update_style(self, *events):
style = {p: getattr(self, p) for p in Layoutable.param if p != 'name'}
margin = style.pop('margin')
if isinstance(margin, tuple):
if len(margin) == 2:
t = b = margin[0]
r = l = margin[1]
else:
t, r, b, l = margin
else:
t = r = b = l = margin
text_margin = (t, 0, 0, l)
slider_margin = (0, r, b, l)
self._text.param.set_param(
margin=text_margin, **{k: v for k, v in style.items() if k != 'style'})
self._slider.param.set_param(margin=slider_margin, **style)

def _sync_value(self, event):
if self._syncing:
return
Expand Down

0 comments on commit c75d6ec

Please sign in to comment.