Spaces:
Running
Running
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 = ( | |
'<div style="color:red;">' | |
+ '<p>JavaScript Error: ' + error.message + '</p>' | |
+ "<p>This usually means there's a typo in your chart specification. " | |
+ "See the javascript console for the full traceback.</p>" | |
+ '</div>' | |
); | |
} | |
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 } | |