Skip to content

Commit

Permalink
Link widgets with same name during embed (#1543)
Browse files Browse the repository at this point in the history
* Link widgets with same name during embed

* Add test
  • Loading branch information
philippjfr committed Sep 17, 2020
1 parent 7be032c commit 06489aa
Show file tree
Hide file tree
Showing 2 changed files with 80 additions and 25 deletions.
56 changes: 37 additions & 19 deletions panel/io/embed.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ def links_to_jslinks(model, widget):
widget.param.trigger(src_spec)
jslinks.append(jslink)
return jslinks


#---------------------------------------------------------------------
# Public API
Expand Down Expand Up @@ -240,8 +240,7 @@ def is_embeddable(object):
if w not in Link.registry]
state_model = State()


widget_data, ignore = [], []
widget_data, merged, ignore = [], {}, []
for widget in widgets:
if widget._param_pane is not None:
# Replace parameter links with JS links
Expand All @@ -263,7 +262,7 @@ def is_embeddable(object):
if not widget._supports_embed or all(w in widget._callbacks for w in get_watchers(widget)):
continue

# Get data which will let us record the changes on widget events
# Get data which will let us record the changes on widget events
w, w_model, vals, getter, on_change, js_getter = widget._get_embed_state(
model, states.get(widget), max_opts)
w_type = w._widget_type
Expand All @@ -273,28 +272,44 @@ def is_embeddable(object):
w_model = w._models[ref][0]
if not isinstance(w_model, w_type):
w_model = w_model.select_one({'type': w_type})

# If there is a widget with the same name, merge with it
if widget.name in merged:
merged[widget.name][0].append(w)
merged[widget.name][1].append(w_model)
continue

js_callback = CustomJS(code=STATE_JS.format(
id=state_model.ref['id'], js_getter=js_getter))
widget_data.append((w, w_model, vals, getter, js_callback, on_change))
widget_data.append(([w], [w_model], vals, getter, js_callback, on_change))
merged[widget.name] = widget_data[-1]

# Ensure we recording state for widgets which could be JS linked
values = []
for (w, w_model, vals, getter, js_callback, on_change) in widget_data:
if w in ignore:
for (ws, w_models, vals, getter, js_callback, on_change) in widget_data:
if ws[0] in ignore:
continue
w_model.js_on_change(on_change, js_callback)
values.append((w, w_model, vals, getter))
for w_model in w_models:
w_model.js_on_change(on_change, js_callback)

# Bidirectionally link models with same name
wm = w_models[0]
for wmo in w_models[1:]:
attr = ws[0]._rename.get('value', 'value')
wm.js_link(attr, wmo, attr)
wmo.js_link(attr, wm, attr)

values.append((ws, w_models, vals, getter))

add_to_doc(model, doc, True)
doc._held_events = []

if not widget_data:
return

restore = [w.value for w, _, _, _ in values]
init_vals = [g(m) for _, m, _, g in values]
restore = [ws[0].value for ws, _, _, _ in values]
init_vals = [g(ms[0]) for _, ms, _, g in values]
cross_product = list(product(*[vals[::-1] for _, _, vals, _ in values]))

if len(cross_product) > max_states:
if config._doc_build:
return
Expand All @@ -312,20 +327,21 @@ def is_embeddable(object):
sub_dict = state_dict
skip = False
for i, k in enumerate(key):
w, m, _, g = values[i]
ws, m, _, g = values[i]
try:
with always_changed(config.safe_embed):
w.value = k
for w in ws:
w.value = k
except Exception:
skip = True
break
sub_dict = sub_dict[g(m)]
sub_dict = sub_dict[g(m[0])]
if skip:
doc._held_events = []
continue

# Drop events originating from widgets being varied
models = [v[1] for v in values]
models = [m for v in values for m in v[1]]
doc._held_events = [e for e in doc._held_events if e.model not in models]
events = record_events(doc)
changes |= events['content'] != '{}'
Expand All @@ -335,9 +351,10 @@ def is_embeddable(object):
if not changes:
return

for (w, _, _, _), v in zip(values, restore):
for (ws, _, _, _), v in zip(values, restore):
try:
w.param.set_param(value=v)
for w in ws:
w.param.set_param(value=v)
except Exception:
pass

Expand All @@ -350,5 +367,6 @@ def is_embeddable(object):
save_path=save_path, load_path=load_path)

state_model.update(json=json, state=state_dict, values=init_vals,
widgets={m.ref['id']: i for i, (_, m, _, _) in enumerate(values)})
widgets={m[0].ref['id']: i for i, (_, m, _, _) in enumerate(values)})
doc.add_root(state_model)
return state_model
49 changes: 43 additions & 6 deletions panel/tests/io/test_embed.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from panel.io.embed import embed_state
from panel.pane import Str
from panel.param import Param
from panel.widgets import Select, FloatSlider, Checkbox
from panel.widgets import IntSlider, Select, FloatSlider, Checkbox, StaticText


def test_embed_param_jslink(document, comm):
Expand Down Expand Up @@ -253,7 +253,7 @@ def test_embed_select_str_jslink(document, comm):
console.log(err)
}
"""

assert cb2.code == """
var value = source['text'];
value = value;
Expand Down Expand Up @@ -330,7 +330,7 @@ def test_embed_checkbox_str_jslink(document, comm):
console.log(err)
}
"""

assert cb2.code == """
var value = source['text'];
value = value;
Expand All @@ -349,7 +349,7 @@ def test_embed_checkbox_str_jslink(document, comm):
}
"""


def test_embed_slider_str_link(document, comm):
slider = FloatSlider(start=0, end=10)
string = Str()
Expand Down Expand Up @@ -408,7 +408,7 @@ def test_embed_slider_str_jslink(document, comm):
console.log(err)
}
"""

assert cb2.code == """
var value = source['text'];
value = value;
Expand All @@ -427,7 +427,44 @@ def test_embed_slider_str_jslink(document, comm):
}
"""



def test_embed_merged_sliders(document, comm):
s1 = IntSlider(name='A', start=1, end=10, value=1)
t1 = StaticText()
s1.param.watch(lambda event: setattr(t1, 'value', event.new), 'value')

s2 = IntSlider(name='A', start=1, end=10, value=1)
t2 = StaticText()
s2.param.watch(lambda event: setattr(t2, 'value', event.new), 'value')

panel = Row(s1, s2, t1, t2)
with config.set(embed=True):
model = panel.get_root(document, comm)
state_model = embed_state(panel, model, document)
assert len(document.roots) == 2
assert model is document.roots[0]

cbs = list(model.select({'type': CustomJS}))
assert len(cbs) == 5

ref1, ref2 = model.children[2].ref['id'], model.children[3].ref['id']
state0 = json.loads(state_model.state[0]['content'])['events']
assert state0 == [
{"attr": "text", "kind": "ModelChanged", "model": {"id": ref1}, "new": "1"},
{"attr": "text", "kind": "ModelChanged", "model": {"id": ref2}, "new": "1"}
]
state1 = json.loads(state_model.state[1]['content'])['events']
assert state1 == [
{"attr": "text", "kind": "ModelChanged", "model": {"id": ref1}, "new": "5"},
{"attr": "text", "kind": "ModelChanged", "model": {"id": ref2}, "new": "5"}
]
state2 = json.loads(state_model.state[2]['content'])['events']
assert state2 == [
{"attr": "text", "kind": "ModelChanged", "model": {"id": ref1}, "new": "9"},
{"attr": "text", "kind": "ModelChanged", "model": {"id": ref2}, "new": "9"}
]


def test_save_embed_bytesio():
checkbox = Checkbox()
string = Str()
Expand Down

0 comments on commit 06489aa

Please sign in to comment.