import { getAttributeOrThrow } from "../lib/attribute"; import { throttle } from "../lib/utils"; /** * Dynamically imports the vega-related modules. */ function importVega() { return import( /* webpackChunkName: "vega" */ "./vega" ); } // See https://github.com/vega/vega-lite/blob/b61b13c2cbd4ecde0448544aff6cdaea721fd22a/src/compile/data/assemble.ts#L228-L231 const DEFAULT_DATASET_NAME = "source_0"; /** * A hook used to render graphics according to the given * Vega-Lite specification. * * The hook expects a `vega_lite::init` event with `{ spec }` payload, * where `spec` is the graphic definition as an object. * * Later `vega_lite::push` events may be sent with `{ data, dataset, window }` payload, * to dynamically update the underlying data. * * Configuration: * * * `data-id` - plot id * */ const VegaLite = { mounted() { this.props = getProps(this); this.state = { container: null, viewPromise: null, }; this.state.container = document.createElement("div"); this.el.appendChild(this.state.container); this.handleEvent(`vega_lite:${this.props.id}:init`, ({ spec }) => { if (!spec.data) { spec.data = { values: [] }; } this.state.viewPromise = importVega() .then(({ vegaEmbed }) => { return vegaEmbed(this.state.container, spec, {}); }) .then((result) => result.view) .catch((error) => { const message = `Failed to render the given Vega-Lite specification, got the following error:\n\n ${error.message}\n\nMake sure to check for typos.`; this.state.container.innerHTML = `
${message}
`; }); }); const throttledResize = throttle((view) => view.resize(), 1_000); this.handleEvent( `vega_lite:${this.props.id}:push`, ({ data, dataset, window }) => { dataset = dataset || DEFAULT_DATASET_NAME; this.state.viewPromise.then((view) => { const currentData = view.data(dataset); buildChangeset(currentData, data, window).then((changeset) => { // Schedule resize after the run finishes throttledResize(view); view.change(dataset, changeset).run(); }); }); } ); }, updated() { this.props = getProps(this); }, destroyed() { if (this.state.viewPromise) { this.state.viewPromise.then((view) => view.finalize()); } }, }; function getProps(hook) { return { id: getAttributeOrThrow(hook.el, "data-id"), }; } function buildChangeset(currentData, newData, window) { return importVega().then(({ vega }) => { if (window === 0) { return vega.changeset().remove(currentData); } else if (window) { const toInsert = newData.slice(-window); const freeSpace = Math.max(window - toInsert.length, 0); const toRemove = currentData.slice(0, -freeSpace); return vega.changeset().remove(toRemove).insert(toInsert); } else { return vega.changeset().insert(newData); } }); } export default VegaLite;