let vega = require('vega'); let {truthy} = require('vega-util'); const _ = require("lodash"); let { optimize } = require('svgo'); let fs = require('fs'); // Install custom JSON serializers Object.defineProperty(Date.prototype, "toJSON", {value: function() {return "__$datetime:" + this.getTime()}}) function parseExpression(expr_str) { let expr = vega.parseExpression(expr_str); return JSON.stringify(expr) } function lookupSignalOp(view, name, scope) { // name is an array that may have leading integer group indices var parent_runtime = view._runtime; for (const index of scope) { if (!parent_runtime.subcontext) { throw `Missing subcontext for ${name} with scope ${scope}` } parent_runtime = parent_runtime.subcontext[index]; } return parent_runtime.signals[name]; } function lookupDataOp(view, name, scope) { // name is an array that may have leading integer group indices var parent_runtime = view._runtime; for (const index of scope) { if (!parent_runtime.subcontext) { throw `Missing subcontext for ${name} with scope ${scope}` } parent_runtime = parent_runtime.subcontext[index]; } return parent_runtime.data[name]; } function getWatchValues(view, watches) { var watch_values = []; for (const watch of watches) { let {namespace, name, scope} = watch; if (namespace === "signal") { let signalValue = lookupSignalOp(view, name, scope).value; watch_values.push({watch: {namespace, name, scope}, value: _.clone(signalValue) ?? null}); } else if (namespace === "data") { let dataOp = lookupDataOp(view, name, scope); watch_values.push({watch: {namespace, name, scope}, value: _.clone(dataOp.values.value) ?? []}); } else { throw `Invalid watch namespace: ${namespace}` } } return watch_values } async function evalSpec(spec, watches) { var view = new vega.View(vega.parse(spec), {renderer: 'none'}); await view.runAsync(); return JSON.stringify(getWatchValues(view, watches)); } async function viewToSvgJson(view) { // generate a static SVG image let svg = await view.toSVG(); let svg_opt = optimize(svg, { js2svg: { indent: 2, pretty: true }}).data; return {svg: svg_opt} } async function viewToPngJson(view) { // generate a static SVG image let png = await view.toImageURL('png'); // Remove leading data uri return {png: png.slice('data:image/png;base64,'.length, png.length)} } async function viewToImageJson(view, format) { if (format === "svg") { return await viewToSvgJson(view) } else { return await viewToPngJson(view) } } async function saveViewToImageJson(view, file, format) { let data = await viewToImageJson(view, format); fs.writeFileSync(file, JSON.stringify( data), (err) => { if (err) throw err; }) } async function exportSingle(spec, file, format) { // create a new view instance for a given Vega JSON spec let view = new vega.View(vega.parse(spec), {renderer: 'none'}); await saveViewToImageJson(view, file, format); } async function exportSequence(spec, file, format, init, updates, watches) { // create a new view instance for a given Vega JSON spec var view = new vega.View(vega.parse(spec), {renderer: 'none'}); // Normalize watches watches = watches || []; // Apply initial updates // These updates must be applied before the first run command for (const update of init) { let {namespace, name, scope, value} = update; if (namespace === "signal") { let signalOp = lookupSignalOp(view, name, scope); view.update(signalOp, value); } else if (namespace === "data") { let dataset = lookupDataOp(view, name, scope); let changeset = view.changeset().remove(truthy).insert(value) dataset.modified = true; view.pulse(dataset.input, changeset); } else { throw `Invalid update namespace: ${namespace}` } } // For initial updates, run is not applied until after all init updates are applied await view.runAsync(); // Collect initial watch values let result = [ [await viewToImageJson(view, format), getWatchValues(view, watches)] ]; // Apply iterative updates for (const i of _.range(0, updates.length)) { var update_element = updates[i]; if (!_.isArray(update_element)) { update_element = [update_element]; } for (const update of update_element) { let {namespace, name, scope, value} = update; if (namespace === "signal") { let signalOp = lookupSignalOp(view, name, scope); await view.update(signalOp, value).runAsync(); } else if (namespace === "data") { let dataset = lookupDataOp(view, name, scope); let changeset = view.changeset().remove(truthy).insert(value) dataset.modified = true; await view.pulse(dataset.input, changeset).runAsync(); } else { throw `Invalid update namespace: ${namespace}` } } result.push([await viewToImageJson(view, format), getWatchValues(view, watches)]) } fs.writeFileSync(file, JSON.stringify(result), (err) => { if (err) throw err; }) } module.exports = { parseExpression, evalSpec, exportSingle, exportSequence, }