@perspective-dev/jupyterlab 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/js/view.js ADDED
@@ -0,0 +1,420 @@
1
+ // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2
+ // ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
3
+ // ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
4
+ // ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
5
+ // ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
6
+ // ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7
+ // ┃ Copyright (c) 2017, the Perspective Authors. ┃
8
+ // ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9
+ // ┃ This file is part of the Perspective library, distributed under the terms ┃
10
+ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
+ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
+
13
+ import { DOMWidgetView } from "@jupyter-widgets/base";
14
+ import { PerspectiveJupyterWidget } from "./widget";
15
+
16
+ import perspective from "@perspective-dev/client";
17
+
18
+ function isEqual(a, b) {
19
+ if (a === b) return true;
20
+ if (typeof a != "object" || typeof b != "object" || a == null || b == null)
21
+ return false;
22
+
23
+ let keysA = Object.keys(a),
24
+ keysB = Object.keys(b);
25
+
26
+ if (keysA.length != keysB.length) return false;
27
+ for (let key of keysA) {
28
+ if (!keysB.includes(key)) return false;
29
+ if (typeof a[key] === "function" || typeof b[key] === "function") {
30
+ if (a[key].toString() != b[key].toString()) return false;
31
+ } else {
32
+ if (!isEqual(a[key], b[key])) return false;
33
+ }
34
+ }
35
+
36
+ return true;
37
+ }
38
+
39
+ async function get_psp_wasm_module() {
40
+ let elem = customElements.get("perspective-viewer");
41
+ if (!elem) {
42
+ await customElements.whenDefined("perspective-viewer");
43
+ elem = customElements.get("perspective-viewer");
44
+ }
45
+
46
+ return elem.__wasm_module__;
47
+ }
48
+
49
+ /**
50
+ * `PerspectiveView` defines the plugin's DOM and how the plugin interacts with
51
+ * the DOM.
52
+ */
53
+ export class PerspectiveView extends DOMWidgetView {
54
+ #psp_client_id = `${Math.random()}`;
55
+
56
+ _createElement() {
57
+ const bindingMode = this.model.get("binding_mode");
58
+ this.luminoWidget = new PerspectiveJupyterWidget(
59
+ undefined,
60
+ this,
61
+ bindingMode,
62
+ );
63
+
64
+ // set up perspective_client
65
+ get_psp_wasm_module().then(async (wasm_module) => {
66
+ this.send({
67
+ type: "connect",
68
+ client_id: this.psp_client_id,
69
+ });
70
+
71
+ const { Client } = wasm_module;
72
+ // Responses are fed to the client in the widget's msg:custom handler
73
+ this.perspective_client = new Client(
74
+ async (binary_msg) => {
75
+ const buffer = binary_msg.slice().buffer;
76
+ this.send(
77
+ { type: "binary_msg", client_id: this.psp_client_id },
78
+ [buffer],
79
+ );
80
+ },
81
+ () => {
82
+ this.send({
83
+ type: "hangup",
84
+ client_id: this.psp_client_id,
85
+ });
86
+ },
87
+ );
88
+
89
+ const tableName = this.model.get("table_name");
90
+ if (!tableName) throw new Error("table_name not set in model");
91
+ const table = this.perspective_client
92
+ .open_table(tableName)
93
+ .then(async (table) => {
94
+ if (bindingMode === "client-server") {
95
+ // TODO make this a global lazy singleton
96
+ const client = await perspective.worker();
97
+ const remote_view = await table.view();
98
+ const local_table = await client.table(remote_view);
99
+ return local_table;
100
+ } else if (bindingMode === "server") {
101
+ return table;
102
+ } else {
103
+ throw new Error(`unknown binding mode: ${bindingMode}`);
104
+ }
105
+ });
106
+
107
+ this.luminoWidget.load(table);
108
+ this._restore_from_model();
109
+ });
110
+ this._synchronize_state_dbg = (event) => {
111
+ console.log("perspective-config-update event", event);
112
+ this._synchronize_state();
113
+ };
114
+ this._synchronize_state = this._synchronize_state.bind(this);
115
+
116
+ // add event handler to synchronize traitlet values
117
+ this.luminoWidget.viewer.addEventListener(
118
+ "perspective-config-update",
119
+ this._synchronize_state_dbg,
120
+ );
121
+
122
+ // bind toggle_editable to this
123
+ this._toggle_editable = this._toggle_editable.bind(this);
124
+
125
+ // return the node against witch pWidget is bound
126
+ return this.luminoWidget.node;
127
+ }
128
+
129
+ _setElement(el) {
130
+ if (this.el || el !== this.luminoWidget.node) {
131
+ // Do not allow the view to be reassigned to a different element.
132
+ throw new Error("Cannot reset the DOM element.");
133
+ }
134
+ this.el = this.luminoWidget.node;
135
+ }
136
+
137
+ /**
138
+ * When state changes on the viewer DOM, apply it to the widget state.
139
+ *
140
+ * @param mutations
141
+ */
142
+
143
+ async _synchronize_state(event) {
144
+ if (!this.luminoWidget._load_complete) {
145
+ return;
146
+ }
147
+
148
+ const config = await this.luminoWidget.viewer.save();
149
+
150
+ for (const name of Object.keys(config)) {
151
+ let new_value = config[name];
152
+
153
+ const current_value = this.model.get(name);
154
+ if (typeof new_value === "undefined") {
155
+ continue;
156
+ }
157
+
158
+ if (
159
+ new_value &&
160
+ typeof new_value === "string" &&
161
+ name !== "plugin" &&
162
+ name !== "theme" &&
163
+ name !== "title" &&
164
+ name !== "version"
165
+ ) {
166
+ new_value = JSON.parse(new_value);
167
+ }
168
+
169
+ if (new_value === null && name === "plugin_config") {
170
+ new_value = {};
171
+ }
172
+
173
+ if (!isEqual(new_value, current_value)) {
174
+ this.model.set(name, new_value);
175
+ }
176
+ }
177
+
178
+ // propagate changes back to Python
179
+ this.touch();
180
+ }
181
+
182
+ get psp_client_id() {
183
+ return this.#psp_client_id;
184
+ }
185
+
186
+ /**
187
+ * Attach event handlers to the model for state changes in order to
188
+ * reflect them back to the DOM.
189
+ */
190
+
191
+ render() {
192
+ super.render();
193
+ this.model.on("msg:custom", this._handle_message, this);
194
+ this.model.on("change:plugin", this.plugin_changed, this);
195
+ this.model.on("change:columns", this.columns_changed, this);
196
+ this.model.on("change:group_by", this.group_by_changed, this);
197
+ this.model.on("change:split_by", this.split_by_changed, this);
198
+ this.model.on("change:aggregates", this.aggregates_changed, this);
199
+ this.model.on("change:sort", this.sort_changed, this);
200
+ this.model.on("change:filter", this.filter_changed, this);
201
+ this.model.on("change:expressions", this.expressions_changed, this);
202
+ this.model.on("change:plugin_config", this.plugin_config_changed, this);
203
+ this.model.on("change:theme", this.theme_changed, this);
204
+ this.model.on("change:settings", this.settings_changed, this);
205
+ this.model.on("change:title", this.title_changed, this);
206
+ this.model.on("change:table_name", this.table_name_changed, this);
207
+ }
208
+
209
+ /**
210
+ * Handle messages from the Python PerspectiveViewer instance.
211
+ */
212
+ _handle_message(msg, buffers) {
213
+ if (msg.type === "binary_msg" && msg.client_id === this.psp_client_id) {
214
+ const [dataview] = buffers;
215
+ this.perspective_client.handle_response(dataview.buffer);
216
+ }
217
+ }
218
+
219
+ get client_worker() {
220
+ if (!this._client_worker) {
221
+ this._client_worker = perspective.worker();
222
+ }
223
+ return this._client_worker;
224
+ }
225
+
226
+ async _restore_from_model() {
227
+ await this.luminoWidget.restore({
228
+ plugin: this.model.get("plugin"),
229
+ columns: this.model.get("columns"),
230
+ group_by: this.model.get("group_by"),
231
+ split_by: this.model.get("split_by"),
232
+ aggregates: this.model.get("aggregates"),
233
+ sort: this.model.get("sort"),
234
+ filter: this.model.get("filter"),
235
+ expressions: this.model.get("expressions"),
236
+ plugin_config: this.model.get("plugin_config"),
237
+ theme: this.model.get("theme"),
238
+ settings: this.model.get("settings"),
239
+ title: this.model.get("title"),
240
+ version: this.model.get("version"),
241
+ });
242
+ }
243
+
244
+ // XXX(tom): haven't looked at this, needs testing. used in client-server mode
245
+ async _toggle_editable() {
246
+ // Need to await the table and get the instance
247
+ // separately as load() only takes a promise
248
+ // to a table and not the instance itself.
249
+ const table = await this.luminoWidget.getTable();
250
+
251
+ // Setup ports in advance
252
+ if (!this._client_edit_port) {
253
+ this._client_edit_port = await this.luminoWidget.getEditPort();
254
+ }
255
+
256
+ // if (!this._kernel_edit_port) {
257
+ // this._kernel_edit_port = await this._kernel_table.make_port();
258
+ // }
259
+
260
+ const { plugin_config } = await this.luminoWidget.viewer.save();
261
+ if (plugin_config?.editable) {
262
+ // TODO only evaluated during initial load.
263
+ // Toggling from python after initial load won't
264
+ // cause edits to propagate
265
+
266
+ // Allow edits from the client Perspective to
267
+ // feed back to the kernel.
268
+
269
+ // When the client updates, if the update
270
+ // comes through the edit port then forward
271
+ // it to the server.
272
+ this._client_view_update_callback = (updated) => {
273
+ if (updated.port_id === this._client_edit_port) {
274
+ this._kernel_table.update(updated.delta, {
275
+ port_id: this._kernel_edit_port,
276
+ });
277
+ }
278
+ };
279
+
280
+ // If the server updates, and the edit is
281
+ // not coming from the server edit port,
282
+ // then synchronize state with the client.
283
+ this._kernel_view_update_callback = (updated) => {
284
+ if (updated.port_id !== this._kernel_edit_port) {
285
+ table.update(updated.delta); // any port, we dont care
286
+ }
287
+ };
288
+ } else {
289
+ // ignore
290
+ this._client_view_update_callback = () => {};
291
+
292
+ // Load the table and mirror updates from the
293
+ // kernel.
294
+ this._kernel_view_update_callback = (updated) =>
295
+ table.update(updated.delta);
296
+ }
297
+
298
+ if (this._client_view) {
299
+ // NOTE: if `plugin_config_changed` called before
300
+ // `_handle_load_message`, this will be undefined
301
+ // Ignore, as `_handle_load_message` is sure to
302
+ // follow.
303
+ this._client_view.on_update(
304
+ (updated) => this._client_view_update_callback(updated),
305
+ { mode: "row" },
306
+ );
307
+ }
308
+
309
+ // this._kernel_view.on_update(
310
+ // (updated) => this._kernel_view_update_callback(updated),
311
+ // { mode: "row" }
312
+ // );
313
+ }
314
+
315
+ /**
316
+ * When the View is removed after the widget terminates, clean up the
317
+ * client viewer and Web Worker.
318
+ */
319
+
320
+ remove() {
321
+ // Delete the <perspective-viewer> but do not terminate the shared
322
+ // worker as it is shared across other widgets.
323
+ this.perspective_client.terminate(); // invokes the close callback we wired up in constructor
324
+ this.luminoWidget.delete();
325
+ this.luminoWidget.viewer.removeEventListener(
326
+ "perspective-config-update",
327
+ this._synchronize_state_dbg,
328
+ );
329
+ }
330
+
331
+ /**
332
+ * When traitlets are updated in python, update the corresponding value on
333
+ * the front-end viewer. `client` and `server` are not included, as they
334
+ * are not properties in `<perspective-viewer>`.
335
+ */
336
+
337
+ plugin_changed() {
338
+ this.luminoWidget.restore({
339
+ plugin: this.model.get("plugin"),
340
+ });
341
+ }
342
+
343
+ columns_changed() {
344
+ this.luminoWidget.restore({
345
+ columns: this.model.get("columns"),
346
+ });
347
+ }
348
+
349
+ group_by_changed() {
350
+ this.luminoWidget.restore({
351
+ group_by: this.model.get("group_by"),
352
+ });
353
+ }
354
+
355
+ split_by_changed() {
356
+ this.luminoWidget.restore({
357
+ split_by: this.model.get("split_by"),
358
+ });
359
+ }
360
+
361
+ aggregates_changed() {
362
+ this.luminoWidget.restore({
363
+ aggregates: this.model.get("aggregates"),
364
+ });
365
+ }
366
+
367
+ sort_changed() {
368
+ this.luminoWidget.restore({
369
+ sort: this.model.get("sort"),
370
+ });
371
+ }
372
+
373
+ filter_changed() {
374
+ this.luminoWidget.restore({
375
+ filter: this.model.get("filter"),
376
+ });
377
+ }
378
+
379
+ expressions_changed() {
380
+ this.luminoWidget.restore({
381
+ expressions: this.model.get("expressions"),
382
+ });
383
+ }
384
+
385
+ plugin_config_changed() {
386
+ this.luminoWidget.restore({
387
+ plugin_config: this.model.get("plugin_config"),
388
+ });
389
+ this._toggle_editable();
390
+ }
391
+
392
+ theme_changed() {
393
+ this.luminoWidget.restore({
394
+ theme: this.model.get("theme"),
395
+ });
396
+ }
397
+
398
+ settings_changed() {
399
+ this.luminoWidget.restore({
400
+ settings: this.model.get("settings"),
401
+ });
402
+ }
403
+
404
+ title_changed() {
405
+ this.luminoWidget.restore({
406
+ title: this.model.get("title"),
407
+ });
408
+ }
409
+
410
+ version_changed() {
411
+ this.luminoWidget.restore({
412
+ version: this.model.get("version"),
413
+ });
414
+ }
415
+
416
+ table_name_changed() {
417
+ // nop
418
+ // XXX(tom): we may want to re-load the viewer in this instance
419
+ }
420
+ }
@@ -0,0 +1,54 @@
1
+ // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2
+ // ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
3
+ // ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
4
+ // ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
5
+ // ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
6
+ // ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7
+ // ┃ Copyright (c) 2017, the Perspective Authors. ┃
8
+ // ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9
+ // ┃ This file is part of the Perspective library, distributed under the terms ┃
10
+ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
+ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
+
13
+ import { PerspectiveWidget } from "./psp_widget";
14
+
15
+ /**
16
+ * PerspectiveJupyterWidget is the ipywidgets front-end for the Perspective Jupyterlab plugin.
17
+ */
18
+ export class PerspectiveJupyterWidget extends PerspectiveWidget {
19
+ constructor(name = "Perspective", view, bindingMode) {
20
+ super(name, view.el, bindingMode);
21
+ this._view = view;
22
+ }
23
+
24
+ /**
25
+ * Process the lumino message.
26
+ *
27
+ * Any custom lumino widget used inside a Jupyter widget should override
28
+ * the processMessage function like this.
29
+ */
30
+
31
+ processMessage(msg) {
32
+ super.processMessage(msg);
33
+ this._view.processLuminoMessage(msg);
34
+ }
35
+
36
+ /**
37
+ * Dispose the widget.
38
+ *
39
+ * This causes the view to be destroyed as well with 'remove'
40
+ */
41
+
42
+ dispose() {
43
+ if (this.isDisposed) {
44
+ return;
45
+ }
46
+
47
+ super.dispose();
48
+ if (this._view) {
49
+ this._view.remove();
50
+ }
51
+
52
+ this._view = null;
53
+ }
54
+ }
@@ -0,0 +1,53 @@
1
+ // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2
+ // ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
3
+ // ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
4
+ // ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
5
+ // ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
6
+ // ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7
+ // ┃ Copyright (c) 2017, the Perspective Authors. ┃
8
+ // ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9
+ // ┃ This file is part of the Perspective library, distributed under the terms ┃
10
+ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
+ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
+
13
+ @import "@perspective-dev/viewer/dist/css/themes.css";
14
+
15
+ div.PSPContainer {
16
+ overflow: auto;
17
+ padding-right: 5px;
18
+ padding-bottom: 5px;
19
+ height: 100%;
20
+ width: 100%;
21
+ flex: 1;
22
+ }
23
+
24
+ .jp-Notebook div.PSPContainer {
25
+ resize: vertical;
26
+ }
27
+
28
+ // Widget height for Jupyterlab
29
+ .jp-NotebookPanel-notebook div.PSPContainer {
30
+ height: 520px;
31
+ }
32
+
33
+ // Widget height for Jupyter Notebook
34
+ .jupyter-widgets-view div.PSPContainer {
35
+ height: 520px;
36
+ }
37
+
38
+ // Widget height for Voila
39
+ // Widget height for VS Code
40
+ body[data-voila="voila"] .jp-OutputArea-output div.PSPContainer,
41
+ body[data-vscode-theme-id] .cell-output-ipywidget-background div.PSPContainer {
42
+ min-height: 520px;
43
+ height: 520px;
44
+ }
45
+
46
+ div.PSPContainer perspective-viewer[theme="Pro Light"] {
47
+ --plugin--border: 1px solid #e0e0e0;
48
+ }
49
+
50
+ div.PSPContainer perspective-viewer {
51
+ display: block;
52
+ height: 100%;
53
+ }