|
| 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 from "@perspective-dev/client"; |
| 14 | +import perspective_viewer from "@perspective-dev/viewer"; |
| 15 | + |
| 16 | +import server_wasm from "@perspective-dev/server/dist/wasm/perspective-server.wasm"; |
| 17 | +import client_wasm from "@perspective-dev/viewer/dist/wasm/perspective-viewer.wasm"; |
| 18 | + |
| 19 | +import "@perspective-dev/viewer-datagrid"; |
| 20 | +import "@perspective-dev/viewer-charts"; |
| 21 | + |
| 22 | +const ready = Promise.all([ |
| 23 | + perspective_viewer.init_client(client_wasm), |
| 24 | + perspective.init_server(server_wasm), |
| 25 | +]); |
| 26 | + |
| 27 | +export { ready }; |
| 28 | + |
| 29 | +export async function worker() { |
| 30 | + await ready; |
| 31 | + return await perspective.worker(); |
| 32 | +} |
| 33 | + |
| 34 | +const PERSISTENT_ATTRIBUTES = [ |
| 35 | + "plugin", |
| 36 | + "columns", |
| 37 | + "group_by", |
| 38 | + "split_by", |
| 39 | + "aggregates", |
| 40 | + "sort", |
| 41 | + "filter", |
| 42 | + "expressions", |
| 43 | + "plugin_config", |
| 44 | + "settings", |
| 45 | + "theme", |
| 46 | + "title", |
| 47 | + "version", |
| 48 | +]; |
| 49 | + |
| 50 | +function isEqual(a, b) { |
| 51 | + if (a === b) return true; |
| 52 | + if (typeof a != "object" || typeof b != "object" || a == null || b == null) |
| 53 | + return false; |
| 54 | + |
| 55 | + const keysA = Object.keys(a); |
| 56 | + const keysB = Object.keys(b); |
| 57 | + if (keysA.length != keysB.length) return false; |
| 58 | + for (const key of keysA) { |
| 59 | + if (!keysB.includes(key)) return false; |
| 60 | + if (typeof a[key] === "function" || typeof b[key] === "function") { |
| 61 | + if (a[key].toString() != b[key].toString()) return false; |
| 62 | + } else { |
| 63 | + if (!isEqual(a[key], b[key])) return false; |
| 64 | + } |
| 65 | + } |
| 66 | + return true; |
| 67 | +} |
| 68 | + |
| 69 | +async function get_psp_wasm_module() { |
| 70 | + let elem = customElements.get("perspective-viewer"); |
| 71 | + if (!elem) { |
| 72 | + await customElements.whenDefined("perspective-viewer"); |
| 73 | + elem = customElements.get("perspective-viewer"); |
| 74 | + } |
| 75 | + return elem.__wasm_module__; |
| 76 | +} |
| 77 | + |
| 78 | +async function render({ model, el }) { |
| 79 | + await ready; |
| 80 | + |
| 81 | + el.classList.add("PSPContainer"); |
| 82 | + const viewer = document.createElement("perspective-viewer"); |
| 83 | + viewer.classList.add("PSPViewer"); |
| 84 | + viewer.setAttribute("type", "application/psp+json"); |
| 85 | + viewer.addEventListener( |
| 86 | + "contextmenu", |
| 87 | + (event) => event.stopPropagation(), |
| 88 | + false, |
| 89 | + ); |
| 90 | + el.appendChild(viewer); |
| 91 | + |
| 92 | + const client_id = `${Math.random()}`; |
| 93 | + const wasm_module = await get_psp_wasm_module(); |
| 94 | + const psp_client = new wasm_module.Client( |
| 95 | + async (binary_msg) => { |
| 96 | + const buffer = binary_msg.slice().buffer; |
| 97 | + model.send( |
| 98 | + { type: "binary_msg", client_id }, |
| 99 | + null, |
| 100 | + [buffer], |
| 101 | + ); |
| 102 | + }, |
| 103 | + () => { |
| 104 | + model.send({ type: "hangup", client_id }, null); |
| 105 | + }, |
| 106 | + ); |
| 107 | + |
| 108 | + const on_custom_msg = (msg, buffers) => { |
| 109 | + if (msg.type === "binary_msg" && msg.client_id === client_id) { |
| 110 | + const [dataview] = buffers; |
| 111 | + psp_client.handle_response(dataview.buffer); |
| 112 | + } |
| 113 | + }; |
| 114 | + model.on("msg:custom", on_custom_msg); |
| 115 | + model.send({ type: "connect", client_id }, null); |
| 116 | + |
| 117 | + const binding_mode = model.get("binding_mode"); |
| 118 | + const table_name = model.get("table_name"); |
| 119 | + if (!table_name) { |
| 120 | + throw new Error("table_name not set in model"); |
| 121 | + } |
| 122 | + |
| 123 | + let load_complete = false; |
| 124 | + const table_promise = psp_client.open_table(table_name).then(async (t) => { |
| 125 | + if (binding_mode === "client-server") { |
| 126 | + const local_client = await perspective.worker(); |
| 127 | + const remote_view = await t.view(); |
| 128 | + return await local_client.table(remote_view); |
| 129 | + } else if (binding_mode === "server") { |
| 130 | + return t; |
| 131 | + } else { |
| 132 | + throw new Error(`unknown binding mode: ${binding_mode}`); |
| 133 | + } |
| 134 | + }); |
| 135 | + |
| 136 | + await viewer.load(table_promise); |
| 137 | + await viewer.restore( |
| 138 | + Object.fromEntries( |
| 139 | + PERSISTENT_ATTRIBUTES.map((k) => [k, model.get(k)]), |
| 140 | + ), |
| 141 | + ); |
| 142 | + load_complete = true; |
| 143 | + |
| 144 | + const sync_to_python = async () => { |
| 145 | + if (!load_complete) { |
| 146 | + return; |
| 147 | + } |
| 148 | + const config = await viewer.save(); |
| 149 | + for (const name of Object.keys(config)) { |
| 150 | + let new_value = config[name]; |
| 151 | + const current_value = model.get(name); |
| 152 | + if (typeof new_value === "undefined") { |
| 153 | + continue; |
| 154 | + } |
| 155 | + if ( |
| 156 | + new_value && |
| 157 | + typeof new_value === "string" && |
| 158 | + name !== "plugin" && |
| 159 | + name !== "theme" && |
| 160 | + name !== "title" && |
| 161 | + name !== "version" |
| 162 | + ) { |
| 163 | + new_value = JSON.parse(new_value); |
| 164 | + } |
| 165 | + if (new_value === null && name === "plugin_config") { |
| 166 | + new_value = {}; |
| 167 | + } |
| 168 | + if (!isEqual(new_value, current_value)) { |
| 169 | + model.set(name, new_value); |
| 170 | + } |
| 171 | + } |
| 172 | + model.save_changes(); |
| 173 | + }; |
| 174 | + viewer.addEventListener("perspective-config-update", sync_to_python); |
| 175 | + |
| 176 | + const trait_listeners = PERSISTENT_ATTRIBUTES.map((name) => { |
| 177 | + const cb = () => { |
| 178 | + viewer.restore({ [name]: model.get(name) }); |
| 179 | + }; |
| 180 | + model.on(`change:${name}`, cb); |
| 181 | + return [name, cb]; |
| 182 | + }); |
| 183 | + |
| 184 | + return () => { |
| 185 | + for (const [name, cb] of trait_listeners) { |
| 186 | + model.off(`change:${name}`, cb); |
| 187 | + } |
| 188 | + model.off("msg:custom", on_custom_msg); |
| 189 | + viewer.removeEventListener("perspective-config-update", sync_to_python); |
| 190 | + psp_client.terminate(); |
| 191 | + viewer.delete(); |
| 192 | + if (viewer.parentNode === el) { |
| 193 | + el.removeChild(viewer); |
| 194 | + } |
| 195 | + }; |
| 196 | +} |
| 197 | + |
| 198 | +export default { render }; |
0 commit comments