-
-
Notifications
You must be signed in to change notification settings - Fork 402
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
Ensure multiple callbacks do not bleed wrong plot state #1034
Changes from 9 commits
16e9f92
9aa0a0f
ad6df39
4ee3623
cccacfc
7c602cb
6556637
0665086
80afcc8
0ce5e35
eab77cb
3348b11
9b01ca1
2c86401
ce4ad9c
6a3124d
4234142
89921f1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,7 +10,7 @@ | |
from ..comms import JupyterCommJS | ||
|
||
|
||
def attributes_js(attributes): | ||
def attributes_js(attributes, handles): | ||
""" | ||
Generates JS code to look up attributes on JS objects from | ||
an attributes specification dictionary. | ||
|
@@ -28,7 +28,16 @@ def attributes_js(attributes): | |
obj_name = attrs[0] | ||
attr_getters = ''.join(["['{attr}']".format(attr=attr) | ||
for attr in attrs[1:]]) | ||
code += ''.join([data_assign, obj_name, attr_getters, ';\n']) | ||
if obj_name not in ['cb_obj', 'cb_data']: | ||
assign_str = '{assign}{{id: {obj_name}["id"], value: {obj_name}{attr_getters}}};\n'.format( | ||
assign=data_assign, obj_name=obj_name, attr_getters=attr_getters | ||
) | ||
code += 'if (({obj_name} != undefined) && ({obj_name}["id"] == "{id}")) {{ {assign} }}'.format( | ||
obj_name=obj_name, id=handles[obj_name].ref['id'], assign=assign_str | ||
) | ||
else: | ||
assign_str = ''.join([data_assign, obj_name, attr_getters, ';\n']) | ||
code += assign_str | ||
return code | ||
|
||
|
||
|
@@ -76,14 +85,20 @@ class Callback(object): | |
js_callback = """ | ||
function on_msg(msg){{ | ||
msg = JSON.parse(msg.content.data); | ||
var comm = HoloViewsWidget.comms["{comms_target}"]; | ||
var comm_state = HoloViewsWidget.comm_state["{comms_target}"]; | ||
if ("comms_target" in msg) {{ | ||
comms_target = msg["comms_target"] | ||
}} else {{ | ||
comms_target = "{comms_target}" | ||
}} | ||
var comm = HoloViewsWidget.comms[comms_target]; | ||
var comm_state = HoloViewsWidget.comm_state[comms_target]; | ||
if (comm_state.event) {{ | ||
comm.send(comm_state.event); | ||
comm_state.blocked = true; | ||
comm_state.timeout = Date.now()+{debounce}; | ||
}} else {{ | ||
comm_state.blocked = false; | ||
}} | ||
comm_state.timeout = Date.now(); | ||
comm_state.event = undefined; | ||
if ((msg.msg_type == "Ready") && msg.content) {{ | ||
console.log("Python callback returned following output:", msg.content); | ||
|
@@ -92,6 +107,7 @@ class Callback(object): | |
}} | ||
}} | ||
|
||
data['comms_target'] = "{comms_target}"; | ||
var argstring = JSON.stringify(data); | ||
if ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel !== undefined)) {{ | ||
var comm_manager = Jupyter.notebook.kernel.comm_manager; | ||
|
@@ -151,6 +167,7 @@ def __init__(self, plot, streams, source, **params): | |
self.plot = plot | ||
self.streams = streams | ||
self.comm = self._comm_type(plot, on_msg=self.on_msg) | ||
self.stream_handles = defaultdict(list) | ||
self.source = source | ||
|
||
|
||
|
@@ -171,12 +188,23 @@ def initialize(self): | |
|
||
|
||
def on_msg(self, msg): | ||
msg = json.loads(msg) | ||
msg = self._process_msg(msg) | ||
if any(v is None for v in msg.values()): | ||
return | ||
# For each stream check whether plot state is meant for it | ||
# by checking that the IDs match the IDs of the stream's plot | ||
# handles, dispatch only the part of the message meant for | ||
# a particular stream | ||
for stream in self.streams: | ||
stream.update(trigger=False, **msg) | ||
ids = self.stream_handles[stream] | ||
sanitized_msg = {} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this bit sounds like a 'message filter' (as opposed to sanitization) and could be its own method (with docstring). Something like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, sounds good. |
||
for k, v in msg.items(): | ||
if isinstance(v, dict) and 'id' in v: | ||
if v['id'] in ids: | ||
sanitized_msg[k] = v['value'] | ||
else: | ||
sanitized_msg[k] = v | ||
processed_msg = self._process_msg(sanitized_msg) | ||
if not processed_msg: | ||
continue | ||
stream.update(trigger=False, **processed_msg) | ||
Stream.trigger(self.streams) | ||
|
||
|
||
|
@@ -195,23 +223,38 @@ def set_customjs(self, handle): | |
self_callback = self.js_callback.format(comms_target=self.comm.target, | ||
timeout=self.timeout, | ||
debounce=self.debounce) | ||
attributes = attributes_js(self.attributes) | ||
code = 'var data = {};\n' + attributes + self.code + self_callback | ||
|
||
handles = {} | ||
subplots = list(self.plot.subplots.values())[::-1] if self.plot.subplots else [] | ||
plots = [self.plot] + subplots | ||
for plot in plots: | ||
handles.update({k: v for k, v in plot.handles.items() | ||
if k in self.handles}) | ||
|
||
attributes = attributes_js(self.attributes, handles) | ||
code = 'var data = {};\n' + attributes + self.code + self_callback | ||
|
||
# Gather the ids of the plotting handles attached to this callback | ||
# This allows checking that a stream is not given the state | ||
# of a plotting handle it wasn't attached to | ||
stream_handle_ids = defaultdict(list) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Might be good to turn this bit of code into a method ( There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed. |
||
for stream in self.streams: | ||
for h in self.handles: | ||
if h in handles: | ||
handle_id = handles[h].ref['id'] | ||
stream_handle_ids[stream].append(handle_id) | ||
|
||
# Set callback | ||
if id(handle.callback) in self._callbacks: | ||
cb = self._callbacks[id(handle.callback)] | ||
if isinstance(cb, type(self)): | ||
cb.streams += self.streams | ||
for k, v in stream_handle_ids.items(): | ||
cb.stream_handles[k] += v | ||
else: | ||
handle.callback.code += code | ||
else: | ||
self.stream_handles.update(stream_handle_ids) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like the contents of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, it'll stick around but compared to all the plotting state I'm not worried about a few IDs. |
||
js_callback = CustomJS(args=handles, code=code) | ||
self._callbacks[id(js_callback)] = self | ||
handle.callback = js_callback | ||
|
@@ -249,8 +292,12 @@ class RangeXYCallback(Callback): | |
handles = ['x_range', 'y_range'] | ||
|
||
def _process_msg(self, msg): | ||
return {'x_range': (msg['x0'], msg['x1']), | ||
'y_range': (msg['y0'], msg['y1'])} | ||
data = {} | ||
if 'x0' in msg and 'x1' in msg: | ||
data['x_range'] = (msg['x0'], msg['x1']) | ||
if 'y0' in msg and 'y1' in msg: | ||
data['y_range'] = (msg['y0'], msg['y1']) | ||
return data | ||
|
||
|
||
class RangeXCallback(Callback): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All of the new bits of code below seems to have the form:
If you agree this is a general pattern, maybe we can just have an class RangeXCallback(Callback):
handles = ['x_range']
def applicable(msg):
return 'x0' in msg and 'x1' in msg
def _process_msg(self, msg):
return {'x_range': (msg['x0'], msg['x1'])} And of course There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if there are multiple predicates, such as in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you agree with this suggestion, maybe it should be made into a new issue (feature request)? I don't think it would be hard to implement later. |
||
|
@@ -261,7 +308,10 @@ class RangeXCallback(Callback): | |
handles = ['x_range'] | ||
|
||
def _process_msg(self, msg): | ||
return {'x_range': (msg['x0'], msg['x1'])} | ||
if 'x0' in msg and 'x1' in msg: | ||
return {'x_range': (msg['x0'], msg['x1'])} | ||
else: | ||
return {} | ||
|
||
|
||
class RangeYCallback(Callback): | ||
|
@@ -272,7 +322,10 @@ class RangeYCallback(Callback): | |
handles = ['y_range'] | ||
|
||
def _process_msg(self, msg): | ||
return {'y_range': (msg['y0'], msg['y1'])} | ||
if 'y0' in msg and 'y1' in msg: | ||
return {'y_range': (msg['y0'], msg['y1'])} | ||
else: | ||
return {} | ||
|
||
|
||
class BoundsCallback(Callback): | ||
|
@@ -285,7 +338,10 @@ class BoundsCallback(Callback): | |
handles = ['box_select'] | ||
|
||
def _process_msg(self, msg): | ||
return {'bounds': (msg['x0'], msg['y0'], msg['x1'], msg['y1'])} | ||
if all(c in msg for c in ['x0', 'y0', 'x1', 'y1']): | ||
return {'bounds': (msg['x0'], msg['y0'], msg['x1'], msg['y1'])} | ||
else: | ||
return {} | ||
|
||
|
||
class Selection1DCallback(Callback): | ||
|
@@ -295,7 +351,10 @@ class Selection1DCallback(Callback): | |
handles = ['source'] | ||
|
||
def _process_msg(self, msg): | ||
return {'index': [int(v) for v in msg['index']]} | ||
if 'index' in msg: | ||
return {'index': [int(v) for v in msg['index']]} | ||
else: | ||
return {} | ||
|
||
|
||
callbacks = Stream._callbacks['bokeh'] | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be good to see the docstring updated with an example showing how handles is involved...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This docstring still needs to be updated I think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, still need to do that.