Skip to content

Commit 7de271e

Browse files
authored
Add option to combine events and support other events (#102)
* Add option to combine events * Added docs * Fixed linting issues * Handle other events * Fixed formatting * Attempt to fix travis * Fixed linting * Fix for travis conda env * Improve typing
1 parent 6b2c6ef commit 7de271e

File tree

3 files changed

+163
-18
lines changed

3 files changed

+163
-18
lines changed

.travis.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,17 @@ cache:
66
install:
77
- wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh
88
- bash miniconda.sh -b -p $HOME/miniconda
9-
- export PATH="$HOME/miniconda/bin:$PATH"
9+
- source "$HOME/miniconda/etc/profile.d/conda.sh"
1010
- hash -r
1111
- conda config --set always_yes yes
1212
- conda config --set quiet yes
1313
- conda config --set changeps1 no
1414
- conda config --add channels bokeh
1515
- conda config --add channels conda-forge
16+
- conda update -q conda
1617
- conda info -a
17-
- conda install conda-build nodejs jupyterlab notebook
18+
- conda create -n test_env conda-build nodejs jupyterlab notebook
19+
- conda activate test_env
1820
- npm set progress false
1921
- npm install -g npm
2022
- conda build conda.recipe/

jupyter_bokeh/widgets.py

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,17 @@
1919

2020
# External imports
2121
from ipywidgets import DOMWidget
22-
from traitlets import Unicode, Dict
22+
from traitlets import Bool, Dict, Unicode
2323

2424
# Bokeh imports
2525
from bokeh.core.json_encoder import serialize_json
26-
from bokeh.models import LayoutDOM
2726
from bokeh.document import Document
28-
from bokeh.protocol import Protocol
29-
from bokeh.util.dependencies import import_optional
3027
from bokeh.embed.elements import div_for_render_item
3128
from bokeh.embed.util import standalone_docs_json_and_render_items
29+
from bokeh.events import Event
30+
from bokeh.models import LayoutDOM
31+
from bokeh.protocol import Protocol
32+
from bokeh.util.dependencies import import_optional
3233

3334
#-----------------------------------------------------------------------------
3435
# Globals and constants
@@ -55,6 +56,7 @@ class BokehModel(DOMWidget):
5556
_view_module = Unicode(_module_name).tag(sync=True)
5657
_view_module_version = Unicode(_module_version).tag(sync=True)
5758

59+
combine_events = Bool(False).tag(sync=True)
5860
render_bundle = Dict().tag(sync=True, to_json=lambda obj, _: serialize_json(obj))
5961

6062
@property
@@ -104,15 +106,31 @@ def _document_patched(self, event):
104106
def _sync_model(self, _, content, _buffers):
105107
if content.get("event", "") != "jsevent":
106108
return
107-
new, old, attr = content["new"], content["old"], content["attr"]
108-
submodel = self._model.select_one({"id": content["id"]})
109-
descriptor = submodel.lookup(content['attr'])
110-
try:
111-
descriptor._real_set(submodel, old, new, setter=self)
112-
except Exception:
113-
return
114-
for cb in submodel._callbacks.get(attr, []):
115-
cb(attr, old, new)
109+
kind = content.get("kind")
110+
if kind == 'ModelChanged':
111+
hint = content.get("hint")
112+
if hint:
113+
cds = self._model.select_one({"id": hint["column_source"]["id"]})
114+
if "patches" in hint:
115+
# Handle ColumnsPatchedEvent
116+
cds.patch(hint["patches"], setter=self)
117+
elif "data" in hint:
118+
# Handle ColumnsStreamedEvent
119+
cds._stream(hint["data"], rollover=hint["rollover"], setter=self)
120+
return
121+
122+
# Handle ModelChangedEvent
123+
new, old, attr = content["new"], content["old"], content["attr"]
124+
submodel = self._model.select_one({"id": content["id"]})
125+
descriptor = submodel.lookup(content['attr'])
126+
try:
127+
descriptor._real_set(submodel, old, new, hint=hint, setter=self)
128+
except Exception:
129+
return
130+
for cb in submodel._callbacks.get(attr, []):
131+
cb(attr, old, new)
132+
elif kind == 'MessageSent':
133+
self._document.apply_json_event(content["msg_data"])
116134

117135
#-----------------------------------------------------------------------------
118136
# Dev API

src/widgets.ts

Lines changed: 128 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,34 @@ export type RenderBundle = {
2727
div: string
2828
}
2929

30+
export interface DocumentChanged {
31+
event: "jsevent"
32+
kind: string
33+
}
34+
35+
export interface ModelChanged extends DocumentChanged {
36+
event: "jsevent"
37+
kind: "ModelChanged"
38+
id: string
39+
new: unknown
40+
attr: string
41+
old: unknown
42+
hint: unknown
43+
}
44+
45+
export interface MessageSent extends DocumentChanged {
46+
event: "jsevent"
47+
kind: "MessageSent"
48+
msg_data: {
49+
event_name: string,
50+
event_values: {
51+
model: {id: string},
52+
[other: string]: any
53+
}
54+
}
55+
msg_type: string
56+
}
57+
3058
export class BokehModel extends DOMWidgetModel {
3159
defaults() {
3260
return {
@@ -40,6 +68,7 @@ export class BokehModel extends DOMWidgetModel {
4068
_view_module: name,
4169
_view_module_version: version_range,
4270

71+
combine_events: false,
4372
render_bundle: {},
4473
}
4574
}
@@ -53,17 +82,46 @@ export class BokehView extends DOMWidgetView {
5382
private _document: Document | null
5483
private _receiver: Receiver
5584
private _blocked: boolean
85+
private _msgs: any[]
86+
private _idle: boolean
87+
private _combine: boolean
5688

5789
constructor(options?: any) {
5890
super(options)
5991
this._document = null
6092
this._blocked = false
93+
this._idle = true
94+
this._combine = true
95+
this._msgs = []
6196
const {Receiver} = bk_require("protocol/receiver")
6297
this._receiver = new Receiver()
6398
this.model.on("change:render_bundle", () => this.render())
99+
if ((window as any).Jupyter != null && (window as any).Jupyter.notebook != null) {
100+
// Handle classic Jupyter notebook
101+
const events = (window as any).require('base/js/events')
102+
events.on('kernel_idle.Kernel', () => this._process_msg())
103+
} else if ((this.model.widget_manager as any).context != null) {
104+
// Handle JupyterLab
105+
(this.model.widget_manager as any).context.sessionContext.statusChanged.connect((_: any, status: any) => {
106+
if (status === "idle")
107+
this._process_msg()
108+
})
109+
} else {
110+
if (this.model.get("combine_events"))
111+
console.warn("BokehView cannot combine events because Kernel idle status cannot be determined.")
112+
this._combine = false
113+
}
64114
this.listenTo(this.model, "msg:custom", (content, buffers) => this._consume_patch(content, buffers))
65115
}
66116

117+
protected _process_msg(): void {
118+
if (this._msgs.length == 0) {
119+
this._idle = true
120+
return
121+
}
122+
this.send(this._msgs.shift())
123+
}
124+
67125
render(): void {
68126
const bundle = JSON.parse(this.model.get("render_bundle"))
69127
const {docs_json, render_items, div} = bundle as RenderBundle
@@ -82,10 +140,77 @@ export class BokehView extends DOMWidgetView {
82140
this._document.on_change((event: any) => this._change_event(event))
83141
}
84142

143+
_combine_events(new_msg: (ModelChanged | MessageSent)): (ModelChanged | MessageSent)[] {
144+
const new_msgs = []
145+
for (const msg of this._msgs) {
146+
if (new_msg.kind != msg.kind)
147+
new_msgs.push(msg)
148+
else if (msg.kind == "ModelChanged" && new_msg.kind == "ModelChanged") {
149+
if (msg.id != new_msg.id || msg.attr != new_msg.attr)
150+
new_msgs.push(msg)
151+
} else if (msg.kind == "MessageSent" && new_msg.kind == "MessageSent") {
152+
if (
153+
msg.msg_data.event_values.model.id != new_msg.msg_data.event_values.model.id ||
154+
msg.msg_data.event_name != new_msg.msg_data.event_name
155+
)
156+
new_msgs.push(msg)
157+
}
158+
}
159+
new_msgs.push(new_msg)
160+
return new_msgs
161+
}
162+
163+
_send(msg: (ModelChanged | MessageSent)): void {
164+
if (!this._idle && this._combine && this.model.get("combine_events"))
165+
// Queue event and drop previous events on same model attribute
166+
this._msgs = this._combine_events(msg)
167+
else {
168+
this._idle = false
169+
this.send(msg)
170+
}
171+
}
172+
85173
protected _change_event(event: DocumentChangedEvent): void {
86-
const {ModelChangedEvent} = bk_require("document/events")
87-
if (!this._blocked && event instanceof ModelChangedEvent)
88-
this.send({event: "jsevent", id: event.model.id, new: event.new_, attr: event.attr, old: event.old})
174+
if (this._blocked) { return }
175+
const {ModelChangedEvent, MessageSentEvent} = bk_require("document/events")
176+
if (event instanceof ModelChangedEvent) {
177+
const js_msg: ModelChanged = {
178+
event: "jsevent",
179+
kind: "ModelChanged",
180+
id: event.model.id,
181+
attr: event.attr,
182+
new: event.new_,
183+
old: event.old,
184+
hint: null,
185+
}
186+
if (event.hint != null) {
187+
if (event.hint.patches != null) {
188+
js_msg["hint"] = {
189+
column_source: event.hint.column_source,
190+
patches: event.hint.patches,
191+
}
192+
} else if (event.hint.data != null) {
193+
js_msg["hint"] = {
194+
column_source: event.hint.column_source,
195+
data: event.hint.data,
196+
rollover: event.hint.rollover,
197+
}
198+
}
199+
}
200+
this._send(js_msg)
201+
} else if ((event instanceof MessageSentEvent) && (event.msg_type == "bokeh_event")) {
202+
const msg_data = {...event.msg_data}
203+
const event_values = {...msg_data.event_values}
204+
event_values["model"] = {id: event_values.model.id}
205+
msg_data["event_values"] = event_values
206+
const js_msg: MessageSent = {
207+
event: "jsevent",
208+
kind: "MessageSent",
209+
msg_type: event.msg_type,
210+
msg_data: msg_data,
211+
}
212+
this._send(js_msg)
213+
}
89214
}
90215

91216
protected _consume_patch(content: {msg: "patch", payload?: Fragment}, buffers: DataView[]): void {

0 commit comments

Comments
 (0)