import vegaEmbed from "https://esm.sh/vega-embed@6?deps=vega@5&deps=vega-lite@5.20.1"; import lodashDebounce from "https://esm.sh/lodash-es@4.17.21/debounce"; // Note: For offline support, the import lines above are removed and the remaining script // is bundled using vl-convert's javascript_bundle function. See the documentation of // the javascript_bundle function for details on the available imports and their names. // If an additional import is required in the future, it will need to be added to vl-convert // in order to preserve offline support. async function render({ model, el }) { let finalize; function showError(error){ el.innerHTML = ( '
' + '

JavaScript Error: ' + error.message + '

' + "

This usually means there's a typo in your chart specification. " + "See the javascript console for the full traceback.

" + '
' ); } const reembed = async () => { if (finalize != null) { finalize(); } model.set("local_tz", Intl.DateTimeFormat().resolvedOptions().timeZone); let spec = structuredClone(model.get("spec")); if (spec == null) { // Remove any existing chart and return while (el.firstChild) { el.removeChild(el.lastChild); } model.save_changes(); return; } let embedOptions = structuredClone(model.get("embed_options")) ?? undefined; let api; try { api = await vegaEmbed(el, spec, embedOptions); } catch (error) { showError(error) return; } finalize = api.finalize; // Debounce config const wait = model.get("debounce_wait") ?? 10; const debounceOpts = {leading: false, trailing: true}; if (model.get("max_wait") ?? true) { debounceOpts["maxWait"] = wait; } const initialSelections = {}; for (const selectionName of Object.keys(model.get("_vl_selections"))) { const storeName = `${selectionName}_store`; const selectionHandler = (_, value) => { const newSelections = cleanJson(model.get("_vl_selections") ?? {}); const store = cleanJson(api.view.data(storeName) ?? []); newSelections[selectionName] = {value, store}; model.set("_vl_selections", newSelections); model.save_changes(); }; api.view.addSignalListener(selectionName, lodashDebounce(selectionHandler, wait, debounceOpts)); initialSelections[selectionName] = { value: cleanJson(api.view.signal(selectionName) ?? {}), store: cleanJson(api.view.data(storeName) ?? []) } } model.set("_vl_selections", initialSelections); const initialParams = {}; for (const paramName of Object.keys(model.get("_params"))) { const paramHandler = (_, value) => { const newParams = JSON.parse(JSON.stringify(model.get("_params"))) || {}; newParams[paramName] = value; model.set("_params", newParams); model.save_changes(); }; api.view.addSignalListener(paramName, lodashDebounce(paramHandler, wait, debounceOpts)); initialParams[paramName] = api.view.signal(paramName) ?? null } model.set("_params", initialParams); model.save_changes(); // Param change callback model.on('change:_params', async (new_params) => { for (const [param, value] of Object.entries(new_params.changed._params)) { api.view.signal(param, value); } await api.view.runAsync(); }); // Add signal/data listeners for (const watch of model.get("_js_watch_plan") ?? []) { if (watch.namespace === "data") { const dataHandler = (_, value) => { model.set("_js_to_py_updates", [{ namespace: "data", name: watch.name, scope: watch.scope, value: cleanJson(value) }]); model.save_changes(); }; addDataListener(api.view, watch.name, watch.scope, lodashDebounce(dataHandler, wait, debounceOpts)) } else if (watch.namespace === "signal") { const signalHandler = (_, value) => { model.set("_js_to_py_updates", [{ namespace: "signal", name: watch.name, scope: watch.scope, value: cleanJson(value) }]); model.save_changes(); }; addSignalListener(api.view, watch.name, watch.scope, lodashDebounce(signalHandler, wait, debounceOpts)) } } // Add signal/data updaters model.on('change:_py_to_js_updates', async (updates) => { for (const update of updates.changed._py_to_js_updates ?? []) { if (update.namespace === "signal") { setSignalValue(api.view, update.name, update.scope, update.value); } else if (update.namespace === "data") { setDataValue(api.view, update.name, update.scope, update.value); } } await api.view.runAsync(); }); } model.on('change:spec', reembed); model.on('change:embed_options', reembed); model.on('change:debounce_wait', reembed); model.on('change:max_wait', reembed); await reembed(); } function cleanJson(data) { return JSON.parse(JSON.stringify(data)) } function getNestedRuntime(view, scope) { var runtime = view._runtime; for (const index of scope) { runtime = runtime.subcontext[index]; } return runtime } function lookupSignalOp(view, name, scope) { let parent_runtime = getNestedRuntime(view, scope); return parent_runtime.signals[name] ?? null; } function dataRef(view, name, scope) { let parent_runtime = getNestedRuntime(view, scope); return parent_runtime.data[name]; } export function setSignalValue(view, name, scope, value) { let signal_op = lookupSignalOp(view, name, scope); view.update(signal_op, value); } export function setDataValue(view, name, scope, value) { let dataset = dataRef(view, name, scope); let changeset = view.changeset().remove(() => true).insert(value) dataset.modified = true; view.pulse(dataset.input, changeset); } export function addSignalListener(view, name, scope, handler) { let signal_op = lookupSignalOp(view, name, scope); return addOperatorListener( view, name, signal_op, handler, ); } export function addDataListener(view, name, scope, handler) { let dataset = dataRef(view, name, scope).values; return addOperatorListener( view, name, dataset, handler, ); } // Private helpers from Vega for dealing with nested signals/data function findOperatorHandler(op, handler) { const h = (op._targets || []) .filter(op => op._update && op._update.handler === handler); return h.length ? h[0] : null; } function addOperatorListener(view, name, op, handler) { let h = findOperatorHandler(op, handler); if (!h) { h = trap(view, () => handler(name, op.value)); h.handler = handler; view.on(op, null, h); } return view; } function trap(view, fn) { return !fn ? null : function() { try { fn.apply(this, arguments); } catch (error) { view.error(error); } }; } export default { render }