diff --git a/panel/pane/holoviews.py b/panel/pane/holoviews.py index 33fd387fc8..d2afb3299b 100644 --- a/panel/pane/holoviews.py +++ b/panel/pane/holoviews.py @@ -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 @@ -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.""") @@ -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) @@ -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 @@ -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 @@ -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 @@ -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)) @@ -245,7 +273,7 @@ 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: @@ -253,6 +281,8 @@ def widgets_from_dimensions(cls, object, widget_types={}, widgets_type='individu '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) diff --git a/panel/tests/pane/test_holoviews.py b/panel/tests/pane/test_holoviews.py index 51cb1ab756..a33ad8d9a2 100644 --- a/panel/tests/pane/test_holoviews.py +++ b/panel/tests/pane/test_holoviews.py @@ -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']) diff --git a/panel/widgets/slider.py b/panel/widgets/slider.py index d26941e2da..cc9012cfe9 100644 --- a/panel/widgets/slider.py +++ b/panel/widgets/slider.py @@ -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 @@ -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 @@ -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') @@ -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