Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Interactive layout fixes #136

Merged
merged 8 commits into from
Nov 6, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions panel/interact.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,8 @@ def __init__(self, object, params={}, **kwargs):
self._inner_layout = Row(self._pane)
self.widget_box = WidgetBox(*(widget for _, widget in widgets
if isinstance(widget, Widget)))
self.layout.objects = [self.widget_box, self]
self.layout.objects = [self.widget_box, self._inner_layout]
self._link_widgets()

@property
def kwargs(self):
Expand Down Expand Up @@ -170,10 +171,9 @@ def find_abbreviations(self, kwargs):

def _get_model(self, doc, root=None, parent=None, comm=None):
layout = self._inner_layout._get_model(doc, root, parent, comm)
self._link_widgets(layout, doc, root, parent, comm)
return layout

def _link_widgets(self, layout, doc, root, parent, comm):
def _link_widgets(self):
if self.manual_update:
widgets = [('manual', self._widgets['manual'])]
else:
Expand All @@ -199,14 +199,16 @@ def update_pane(change):
new_object._cleanup(None, new_object._temporary)
else:
self._pane.object = new_object
return


# Replace pane entirely
self._pane = Pane(new_object, _temporary=True)
self._inner_layout[0] = self._pane

pname = 'clicks' if name == 'manual' else 'value'
watcher = widget.param.watch(update_pane, pname)
self._callbacks[layout.ref['id']].append(watcher)
self._callbacks['instance'].append(watcher)

def _cleanup(self, model=None, final=False):
self._inner_layout._cleanup(model, final)
Expand Down
24 changes: 18 additions & 6 deletions panel/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,25 @@
from bokeh.models import Box as BkBox
from bokeh.models.widgets import Tabs as BkTabs, Panel as BkPanel

from .util import push, abbreviated_repr
from .util import param_reprs, push
from .viewable import Reactive, Viewable


def has_height(obj):
"""
Whether the supplied layout has a height
"""
if isinstance(obj, BkBox):
for child in obj.children:
if has_height(child):
return True
elif isinstance(obj, BkWidgetBox):
return True
elif getattr(obj, 'height', None) is not None:
return True
return False


class Panel(Reactive):
"""
Abstract baseclass for a layout of Viewables.
Expand Down Expand Up @@ -118,8 +133,7 @@ def _get_model(self, doc, root=None, parent=None, comm=None):
objects = self._get_objects(model, [], doc, root, comm)

# HACK ALERT: Insert Spacer if last item in Column has no height
if (isinstance(self, Column) and objects and not isinstance(objects[-1], (BkWidgetBox, BkBox))
and getattr(objects[-1], 'height', False) is None):
if (isinstance(self, Column) and objects and not has_height(objects[-1])):
objects.append(BkSpacer(height=50))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a parameter default_height? Could be useful e.g. for the report mode dashboards...

Copy link
Member Author

@philippjfr philippjfr Nov 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where would the parameter live, on Column? I'd be happy to add it but maybe in a separate PR.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, height=50 is used here on Panel, so presumably on Panel, so that it can be height=self.default_height

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's specifically behind an isinstance check for Column to avoid having to duplicate the whole method there.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That said I could add an equivalent check for Row:

(isinstance(self, Row) and objects and not any(has_height(o) for o in objects))

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merging this, but I can follow up on this tomorrow.


props = dict(self._init_properties(), objects=objects)
Expand Down Expand Up @@ -147,9 +161,7 @@ def __setitem__(self, index, pane):
def __repr__(self, depth=0):
spacer = '\n' + (' ' * (depth+1))
cls = type(self).__name__
params = ['%s=%s' % (p, abbreviated_repr(v)) for p, v in sorted(self.get_param_values())
if v is not self.params(p).default and v not in ('', None)
and p != 'objects' and not (p == 'name' and v.startswith(cls))]
params = param_reprs(self, ['objects'])
objs = ['[%d] %s' % (i, obj.__repr__(depth+1)) for i, obj in enumerate(self.objects)]
if not params and not objs:
return super(Panel, self).__repr__(depth+1)
Expand Down
6 changes: 2 additions & 4 deletions panel/pane.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from bokeh.models import LayoutDOM, CustomJS, Widget as _BkWidget, Div as _BkDiv

from .layout import Panel, Row
from .util import Div, basestring, push, remove_root, abbreviated_repr
from .util import Div, basestring, param_reprs, push, remove_root
from .viewable import Reactive, Viewable


Expand Down Expand Up @@ -125,9 +125,7 @@ def get_pane_type(cls, obj):

def __repr__(self, depth=0):
cls = type(self).__name__
params = ['%s=%s' % (p, abbreviated_repr(v)) for p, v in sorted(self.get_param_values())
if v is not self.params(p).default and v not in ('', None, {}, [])
and p != 'object' and not (p == 'name' and v.startswith(cls))]
params = param_reprs(self, ['object'])
obj = type(self.object).__name__
template = '{cls}({obj}, {params})' if params else '{cls}({obj})'
return template.format(cls=cls, params=', '.join(params), obj=obj)
Expand Down
18 changes: 11 additions & 7 deletions panel/param.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
from .pane import Pane, PaneBase
from .layout import WidgetBox, Row, Panel, Tabs, Column
from .util import (
default_label_formatter, is_parameterized, get_method_owner,
full_groupby, abbreviated_repr
abbreviated_repr, basestring, default_label_formatter, full_groupby,
get_method_owner, is_parameterized
)
from .widgets import (
LiteralInput, Select, Checkbox, FloatSlider, IntSlider, RangeSlider,
Expand Down Expand Up @@ -141,11 +141,15 @@ def __init__(self, object, **params):
def __repr__(self, depth=0):
cls = type(self).__name__
obj_cls = type(self.object).__name__
params = [k for k in self.object.params() if k != 'name']
params = ['%s=%s' % (p, abbreviated_repr(v)) for p, v in sorted(self.get_param_values())
if v is not self.params(p).default and v not in ('', None, {}, [])
and p != 'object' and not (p == 'name' and v.startswith(obj_cls))
and not (p == 'parameters' and v == params)]
parameters = [k for k in self.object.params() if k != 'name']
params = []
for p, v in sorted(self.get_param_values()):
if v is self.params(p).default: continue
elif v is None: continue
elif isinstance(v, basestring) and v == '': continue
elif p == 'object' or (p == 'name' and v.startswith(obj_cls)): continue
elif p == 'parameters' and v == parameters: continue
params.append('%s=%s' % (p, abbreviated_repr(v)))
obj = type(self.object).__name__
template = '{cls}({obj}, {params})' if params else '{cls}({obj})'
return template.format(cls=cls, params=', '.join(params), obj=obj)
Expand Down
21 changes: 10 additions & 11 deletions panel/tests/test_interact.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from bokeh.models import (Div as BkDiv, Column as BkColumn,
WidgetBox as BkWidgetBox, Paragraph as BkParagraph)
from bokeh.models import Div as BkDiv, Column as BkColumn, WidgetBox as BkWidgetBox

from panel.interact import interactive
from panel import widgets
Expand Down Expand Up @@ -166,7 +165,7 @@ def test(a):

def test_interact_replaces_model(document, comm):
def test(a):
return BkParagraph(text='Pre Test') if a else BkDiv(text='Test')
return 'ABC' if a else BkDiv(text='Test')

interact_pane = interactive(test, a=False)
pane = interact_pane._pane
Expand All @@ -181,20 +180,20 @@ def test(a):
div = box.children[0]
assert isinstance(div, BkDiv)
assert div.text == 'Test'
assert column.children[1].ref['id'] in interact_pane._callbacks
assert len(interact_pane._callbacks['instance']) == 1
assert box.ref['id'] in pane._callbacks

widget.value = True
assert box.ref['id'] not in pane._callbacks
assert pane._callbacks == {}
new_pane = interact_pane._pane
new_box = column.children[1].children[0]
pre = new_box.children[0]
assert isinstance(pre, BkParagraph)
assert pre.text == 'Pre Test'
assert column.children[1].ref['id'] in interact_pane._callbacks
assert new_box.ref['id'] in new_pane._callbacks
assert new_pane is not pane
new_div = column.children[1].children[0]
assert isinstance(new_div, BkDiv)
assert new_div.text == '<p>ABC</p>'
assert len(interact_pane._callbacks['instance']) == 1
assert new_div.ref['id'] in new_pane._callbacks

interact_pane._cleanup(column.children[1])
assert interact_pane._callbacks == {}
assert len(interact_pane._callbacks) == 1
assert pane._callbacks == {}
18 changes: 18 additions & 0 deletions panel/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,24 @@ def abbreviated_repr(value, max_length=25, natural_breaks=(',', ' ')):
return vrepr


def param_reprs(parameterized, skip=[]):
"""
Returns a list of reprs for parameters on the parameterized object.
Skips default and empty values.
"""
cls = type(parameterized).__name__
param_reprs = []
for p, v in sorted(parameterized.get_param_values()):
if v is parameterized.params(p).default: continue
elif v is None: continue
elif isinstance(v, basestring) and v == '': continue
elif isinstance(v, list) and v == []: continue
elif isinstance(v, dict) and v == {}: continue
elif p in skip or (p == 'name' and v.startswith(cls)): continue
param_reprs.append('%s=%s' % (p, abbreviated_repr(v)))
return param_reprs


def full_groupby(l, key=lambda x: x):
"""
Groupby implementation which does not require a prior sort
Expand Down
9 changes: 3 additions & 6 deletions panel/viewable.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from bokeh.server.server import Server
from pyviz_comms import JS_CALLBACK, CommManager, JupyterCommManager

from .util import render_mimebundle, add_to_doc, push, abbreviated_repr
from .util import render_mimebundle, add_to_doc, push, param_reprs


class Viewable(param.Parameterized):
Expand All @@ -48,11 +48,8 @@ def __init__(self, **params):
self._documents = {}

def __repr__(self, depth=0):
cls = type(self).__name__
params = ['%s=%s' % (p, abbreviated_repr(v)) for p, v in sorted(self.get_param_values())
if v is not self.params(p).default and v not in ('', None)
and not (p == 'name' and v.startswith(cls))]
return '{cls}({params})'.format(cls=type(self).__name__, params=', '.join(params))
return '{cls}({params})'.format(cls=type(self).__name__,
params=', '.join(param_reprs(self)))

def __str__(self):
return self.__repr__()
Expand Down