UNPKG

vega-view

Version:

View component and transforms for Vega visualizations.

1,306 lines (1,221 loc) 38.4 kB
import { isString, error, truthy, hasOwnProperty, constant, extend, array, isObject, isArray, toSet, debounce, isDate, inherits, stringValue } from 'vega-util'; import { changeset, isChangeSet, EventStream, transforms, Dataflow, asyncCallback } from 'vega-dataflow'; import { point, renderModule, CanvasHandler, RenderType, Scenegraph } from 'vega-scenegraph'; import { tickStep } from 'd3-array'; import { functionContext } from 'vega-functions'; import { context } from 'vega-runtime'; import { interval } from 'd3-timer'; import { locale } from 'vega-format'; // initialize aria role and label attributes function initializeAria(view) { const el = view.container(); if (el) { el.setAttribute('role', 'graphics-document'); el.setAttribute('aria-roleDescription', 'visualization'); ariaLabel(el, view.description()); } } // update aria-label if we have a DOM container element function ariaLabel(el, desc) { if (el) desc == null ? el.removeAttribute('aria-label') : el.setAttribute('aria-label', desc); } function background (view) { // respond to background signal view.add(null, _ => { view._background = _.bg; view._resize = 1; return _.bg; }, { bg: view._signals.background }); } const Default = 'default'; function cursor (view) { // get cursor signal, add to dataflow if needed const cursor = view._signals.cursor || (view._signals.cursor = view.add({ user: Default, item: null })); // evaluate cursor on each pointermove event view.on(view.events('view', 'pointermove'), cursor, (_, event) => { const value = cursor.value, user = value ? isString(value) ? value : value.user : Default, item = event.item && event.item.cursor || null; return value && user === value.user && item == value.item ? value : { user: user, item: item }; }); // when cursor signal updates, set visible cursor view.add(null, function (_) { let user = _.cursor, item = this.value; if (!isString(user)) { item = user.item; user = user.user; } setCursor(view, user && user !== Default ? user : item || user); return item; }, { cursor: cursor }); } function setCursor(view, cursor) { const el = view.globalCursor() ? typeof document !== 'undefined' && document.body : view.container(); if (el) { return cursor == null ? el.style.removeProperty('cursor') : el.style.cursor = cursor; } } function dataref(view, name) { var data = view._runtime.data; if (!hasOwnProperty(data, name)) { error('Unrecognized data set: ' + name); } return data[name]; } function data(name, values) { return arguments.length < 2 ? dataref(this, name).values.value : change.call(this, name, changeset().remove(truthy).insert(values)); } function change(name, changes) { if (!isChangeSet(changes)) { error('Second argument to changes must be a changeset.'); } const dataset = dataref(this, name); dataset.modified = true; return this.pulse(dataset.input, changes); } function insert(name, _) { return change.call(this, name, changeset().insert(_)); } function remove(name, _) { return change.call(this, name, changeset().remove(_)); } function width(view) { var padding = view.padding(); return Math.max(0, view._viewWidth + padding.left + padding.right); } function height(view) { var padding = view.padding(); return Math.max(0, view._viewHeight + padding.top + padding.bottom); } function offset(view) { var padding = view.padding(), origin = view._origin; return [padding.left + origin[0], padding.top + origin[1]]; } function resizeRenderer(view) { var origin = offset(view), w = width(view), h = height(view); view._renderer.background(view.background()); view._renderer.resize(w, h, origin); view._handler.origin(origin); view._resizeListeners.forEach(handler => { try { handler(w, h); } catch (error) { view.error(error); } }); } /** * Extend an event with additional view-specific methods. * Adds a new property ('vega') to an event that provides a number * of methods for querying information about the current interaction. * The vega object provides the following methods: * view - Returns the backing View instance. * item - Returns the currently active scenegraph item (if any). * group - Returns the currently active scenegraph group (if any). * This method accepts a single string-typed argument indicating the name * of the desired parent group. The scenegraph will be traversed from * the item up towards the root to search for a matching group. If no * argument is provided the enclosing group for the active item is * returned, unless the item it itself a group, in which case it is * returned directly. * xy - Returns a two-element array containing the x and y coordinates for * mouse or touch events. For touch events, this is based on the first * elements in the changedTouches array. This method accepts a single * argument: either an item instance or mark name that should serve as * the reference coordinate system. If no argument is provided the * top-level view coordinate system is assumed. * x - Returns the current x-coordinate, accepts the same arguments as xy. * y - Returns the current y-coordinate, accepts the same arguments as xy. * @param {Event} event - The input event to extend. * @param {Item} item - The currently active scenegraph item (if any). * @return {Event} - The extended input event. */ function eventExtend (view, event, item) { var r = view._renderer, el = r && r.canvas(), p, e, translate; if (el) { translate = offset(view); e = event.changedTouches ? event.changedTouches[0] : event; p = point(e, el); p[0] -= translate[0]; p[1] -= translate[1]; } event.dataflow = view; event.item = item; event.vega = extension(view, item, p); return event; } function extension(view, item, point) { const itemGroup = item ? item.mark.marktype === 'group' ? item : item.mark.group : null; function group(name) { var g = itemGroup, i; if (name) for (i = item; i; i = i.mark.group) { if (i.mark.name === name) { g = i; break; } } return g && g.mark && g.mark.interactive ? g : {}; } function xy(item) { if (!item) return point; if (isString(item)) item = group(item); const p = point.slice(); while (item) { p[0] -= item.x || 0; p[1] -= item.y || 0; item = item.mark && item.mark.group; } return p; } return { view: constant(view), item: constant(item || {}), group: group, xy: xy, x: item => xy(item)[0], y: item => xy(item)[1] }; } const VIEW = 'view', TIMER = 'timer', WINDOW = 'window', NO_TRAP = { trap: false }; /** * Initialize event handling configuration. * @param {object} config - The configuration settings. * @return {object} */ function initializeEventConfig(config) { const events = extend({ defaults: {} }, config); const unpack = (obj, keys) => { keys.forEach(k => { if (isArray(obj[k])) obj[k] = toSet(obj[k]); }); }; unpack(events.defaults, ['prevent', 'allow']); unpack(events, ['view', 'window', 'selector']); return events; } function trackEventListener(view, sources, type, handler) { view._eventListeners.push({ type: type, sources: array(sources), handler: handler }); } function prevent(view, type) { var def = view._eventConfig.defaults, prevent = def.prevent, allow = def.allow; return prevent === false || allow === true ? false : prevent === true || allow === false ? true : prevent ? prevent[type] : allow ? !allow[type] : view.preventDefault(); } function permit(view, key, type) { const rule = view._eventConfig && view._eventConfig[key]; if (rule === false || isObject(rule) && !rule[type]) { view.warn(`Blocked ${key} ${type} event listener.`); return false; } return true; } /** * Create a new event stream from an event source. * @param {object} source - The event source to monitor. * @param {string} type - The event type. * @param {function(object): boolean} [filter] - Event filter function. * @return {EventStream} */ function events(source, type, filter) { var view = this, s = new EventStream(filter), send = function (e, item) { view.runAsync(null, () => { if (source === VIEW && prevent(view, type)) { e.preventDefault(); } s.receive(eventExtend(view, e, item)); }); }, sources; if (source === TIMER) { if (permit(view, 'timer', type)) { view.timer(send, type); } } else if (source === VIEW) { if (permit(view, 'view', type)) { // send traps errors, so use {trap: false} option view.addEventListener(type, send, NO_TRAP); } } else { if (source === WINDOW) { if (permit(view, 'window', type) && typeof window !== 'undefined') { sources = [window]; } } else if (typeof document !== 'undefined') { if (permit(view, 'selector', type)) { sources = Array.from(document.querySelectorAll(source)); } } if (!sources) { view.warn('Can not resolve event source: ' + source); } else { for (var i = 0, n = sources.length; i < n; ++i) { sources[i].addEventListener(type, send); } trackEventListener(view, sources, type, send); } } return s; } function itemFilter(event) { return event.item; } function markTarget(event) { // grab upstream collector feeding the mark operator return event.item.mark.source; } function invoke(name) { return function (_, event) { return event.vega.view().changeset().encode(event.item, name); }; } function hover (hoverSet, leaveSet) { hoverSet = [hoverSet || 'hover']; leaveSet = [leaveSet || 'update', hoverSet[0]]; // invoke hover set upon pointerover this.on(this.events('view', 'pointerover', itemFilter), markTarget, invoke(hoverSet)); // invoke leave set upon pointerout this.on(this.events('view', 'pointerout', itemFilter), markTarget, invoke(leaveSet)); return this; } /** * Finalize a View instance that is being removed. * Cancel any running timers. * Remove all external event listeners. * Remove any currently displayed tooltip. */ function finalize () { var tooltip = this._tooltip, timers = this._timers, handlers = this._handler.handlers(), listeners = this._eventListeners, n, m, e, h, t; n = timers.length; while (--n >= 0) { timers[n].stop(); } n = listeners.length; while (--n >= 0) { e = listeners[n]; m = e.sources.length; while (--m >= 0) { e.sources[m].removeEventListener(e.type, e.handler); } } if (tooltip) { tooltip.call(this, this._handler, null, null, null); } // turn off all registered handlers n = handlers.length; while (--n >= 0) { t = handlers[n].type; h = handlers[n].handler; this._handler.off(t, h); } return this; } function element (tag, attr, text) { const el = document.createElement(tag); for (const key in attr) el.setAttribute(key, attr[key]); if (text != null) el.textContent = text; return el; } const BindClass = 'vega-bind', NameClass = 'vega-bind-name', RadioClass = 'vega-bind-radio'; /** * Bind a signal to an external HTML input element. The resulting two-way * binding will propagate input changes to signals, and propagate signal * changes to the input element state. If this view instance has no parent * element, we assume the view is headless and no bindings are created. * @param {Element|string} el - The parent DOM element to which the input * element should be appended as a child. If string-valued, this argument * will be treated as a CSS selector. If null or undefined, the parent * element of this view will be used as the element. * @param {object} param - The binding parameters which specify the signal * to bind to, the input element type, and type-specific configuration. * @return {View} - This view instance. */ function bind (view, el, binding) { if (!el) return; const param = binding.param; let bind = binding.state; if (!bind) { bind = binding.state = { elements: null, active: false, set: null, update: value => { if (value != view.signal(param.signal)) { view.runAsync(null, () => { bind.source = true; view.signal(param.signal, value); }); } } }; if (param.debounce) { bind.update = debounce(param.debounce, bind.update); } } const create = param.input == null && param.element ? target : generate; create(bind, el, param, view); if (!bind.active) { view.on(view._signals[param.signal], null, () => { bind.source ? bind.source = false : bind.set(view.signal(param.signal)); }); bind.active = true; } return bind; } /** * Bind the signal to an external EventTarget. */ function target(bind, node, param, view) { const type = param.event || 'input'; const handler = () => bind.update(node.value); // initialize signal value to external input value view.signal(param.signal, node.value); // listen for changes on the element node.addEventListener(type, handler); // register with view, so we can remove it upon finalization trackEventListener(view, node, type, handler); // propagate change to element bind.set = value => { node.value = value; node.dispatchEvent(event(type)); }; } function event(type) { return typeof Event !== 'undefined' ? new Event(type) : { type }; } /** * Generate an HTML input form element and bind it to a signal. */ function generate(bind, el, param, view) { const value = view.signal(param.signal); const div = element('div', { 'class': BindClass }); const wrapper = param.input === 'radio' ? div : div.appendChild(element('label')); wrapper.appendChild(element('span', { 'class': NameClass }, param.name || param.signal)); el.appendChild(div); let input = form; switch (param.input) { case 'checkbox': input = checkbox; break; case 'select': input = select; break; case 'radio': input = radio; break; case 'range': input = range; break; } input(bind, wrapper, param, value); } /** * Generates an arbitrary input form element. * The input type is controlled via user-provided parameters. */ function form(bind, el, param, value) { const node = element('input'); for (const key in param) { if (key !== 'signal' && key !== 'element') { node.setAttribute(key === 'input' ? 'type' : key, param[key]); } } node.setAttribute('name', param.signal); node.value = value; el.appendChild(node); node.addEventListener('input', () => bind.update(node.value)); bind.elements = [node]; bind.set = value => node.value = value; } /** * Generates a checkbox input element. */ function checkbox(bind, el, param, value) { const attr = { type: 'checkbox', name: param.signal }; if (value) attr.checked = true; const node = element('input', attr); el.appendChild(node); node.addEventListener('change', () => bind.update(node.checked)); bind.elements = [node]; bind.set = value => node.checked = !!value || null; } /** * Generates a selection list input element. */ function select(bind, el, param, value) { const node = element('select', { name: param.signal }), labels = param.labels || []; param.options.forEach((option, i) => { const attr = { value: option }; if (valuesEqual(option, value)) attr.selected = true; node.appendChild(element('option', attr, (labels[i] || option) + '')); }); el.appendChild(node); node.addEventListener('change', () => { bind.update(param.options[node.selectedIndex]); }); bind.elements = [node]; bind.set = value => { for (let i = 0, n = param.options.length; i < n; ++i) { if (valuesEqual(param.options[i], value)) { node.selectedIndex = i; return; } } }; } /** * Generates a radio button group. */ function radio(bind, el, param, value) { const group = element('span', { 'class': RadioClass }), labels = param.labels || []; el.appendChild(group); bind.elements = param.options.map((option, i) => { const attr = { type: 'radio', name: param.signal, value: option }; if (valuesEqual(option, value)) attr.checked = true; const input = element('input', attr); input.addEventListener('change', () => bind.update(option)); const label = element('label', {}, (labels[i] || option) + ''); label.prepend(input); group.appendChild(label); return input; }); bind.set = value => { const nodes = bind.elements, n = nodes.length; for (let i = 0; i < n; ++i) { if (valuesEqual(nodes[i].value, value)) nodes[i].checked = true; } }; } /** * Generates a slider input element. */ function range(bind, el, param, value) { value = value !== undefined ? value : (+param.max + +param.min) / 2; const max = param.max != null ? param.max : Math.max(100, +value) || 100, min = param.min || Math.min(0, max, +value) || 0, step = param.step || tickStep(min, max, 100); const node = element('input', { type: 'range', name: param.signal, min: min, max: max, step: step }); node.value = value; const span = element('span', {}, +value); el.appendChild(node); el.appendChild(span); const update = () => { span.textContent = node.value; bind.update(+node.value); }; // subscribe to both input and change node.addEventListener('input', update); node.addEventListener('change', update); bind.elements = [node]; bind.set = value => { node.value = value; span.textContent = value; }; } function valuesEqual(a, b) { return a === b || a + '' === b + ''; } function initializeRenderer (view, r, el, constructor, scaleFactor, opt) { r = r || new constructor(view.loader()); return r.initialize(el, width(view), height(view), offset(view), scaleFactor, opt).background(view.background()); } function trap (view, fn) { return !fn ? null : function () { try { fn.apply(this, arguments); } catch (error) { view.error(error); } }; } function initializeHandler (view, prevHandler, el, constructor) { // instantiate scenegraph handler const handler = new constructor(view.loader(), trap(view, view.tooltip())).scene(view.scenegraph().root).initialize(el, offset(view), view); // transfer event handlers if (prevHandler) { prevHandler.handlers().forEach(h => { handler.on(h.type, h.handler); }); } return handler; } function initialize (el, elBind) { const view = this, type = view._renderType, config = view._eventConfig.bind, module = renderModule(type); // containing dom element el = view._el = el ? lookup(view, el, true) : null; // initialize aria attributes initializeAria(view); // select appropriate renderer & handler if (!module) view.error('Unrecognized renderer type: ' + type); const Handler = module.handler || CanvasHandler, Renderer = el ? module.renderer : module.headless; // initialize renderer and input handler view._renderer = !Renderer ? null : initializeRenderer(view, view._renderer, el, Renderer); view._handler = initializeHandler(view, view._handler, el, Handler); view._redraw = true; // initialize signal bindings if (el && config !== 'none') { elBind = elBind ? view._elBind = lookup(view, elBind, true) : el.appendChild(element('form', { 'class': 'vega-bindings' })); view._bind.forEach(_ => { if (_.param.element && config !== 'container') { _.element = lookup(view, _.param.element, !!_.param.input); } }); view._bind.forEach(_ => { bind(view, _.element || elBind, _); }); } return view; } function lookup(view, el, clear) { if (typeof el === 'string') { if (typeof document !== 'undefined') { el = document.querySelector(el); if (!el) { view.error('Signal bind element not found: ' + el); return null; } } else { view.error('DOM document instance not found.'); return null; } } if (el && clear) { try { el.textContent = ''; } catch (e) { el = null; view.error(e); } } return el; } const number = _ => +_ || 0; const paddingObject = _ => ({ top: _, bottom: _, left: _, right: _ }); function padding (_) { return isObject(_) ? { top: number(_.top), bottom: number(_.bottom), left: number(_.left), right: number(_.right) } : paddingObject(number(_)); } /** * Render the current scene in a headless fashion. * This method is asynchronous, returning a Promise instance. * @return {Promise} - A Promise that resolves to a renderer. */ async function renderHeadless (view, type, scaleFactor, opt) { const module = renderModule(type), ctr = module && module.headless; if (!ctr) error('Unrecognized renderer type: ' + type); await view.runAsync(); return initializeRenderer(view, null, null, ctr, scaleFactor, opt).renderAsync(view._scenegraph.root); } /** * Produce an image URL for the visualization. Depending on the type * parameter, the generated URL contains data for either a PNG or SVG image. * The URL can be used (for example) to download images of the visualization. * This method is asynchronous, returning a Promise instance. * @param {string} type - The image type. One of 'svg', 'png' or 'canvas'. * The 'canvas' and 'png' types are synonyms for a PNG image. * @return {Promise} - A promise that resolves to an image URL. */ async function renderToImageURL (type, scaleFactor) { if (type !== RenderType.Canvas && type !== RenderType.SVG && type !== RenderType.PNG) { error('Unrecognized image type: ' + type); } const r = await renderHeadless(this, type, scaleFactor); return type === RenderType.SVG ? toBlobURL(r.svg(), 'image/svg+xml') : r.canvas().toDataURL('image/png'); } function toBlobURL(data, mime) { const blob = new Blob([data], { type: mime }); return window.URL.createObjectURL(blob); } /** * Produce a Canvas instance containing a rendered visualization. * This method is asynchronous, returning a Promise instance. * @return {Promise} - A promise that resolves to a Canvas instance. */ async function renderToCanvas (scaleFactor, opt) { const r = await renderHeadless(this, RenderType.Canvas, scaleFactor, opt); return r.canvas(); } /** * Produce a rendered SVG string of the visualization. * This method is asynchronous, returning a Promise instance. * @return {Promise} - A promise that resolves to an SVG string. */ async function renderToSVG (scaleFactor) { const r = await renderHeadless(this, RenderType.SVG, scaleFactor); return r.svg(); } function runtime (view, spec, expr) { return context(view, transforms, functionContext, expr).parse(spec); } function scale(name) { var scales = this._runtime.scales; if (!hasOwnProperty(scales, name)) { error('Unrecognized scale or projection: ' + name); } return scales[name].value; } var Width = 'width', Height = 'height', Padding = 'padding', Skip = { skip: true }; function viewWidth(view, width) { var a = view.autosize(), p = view.padding(); return width - (a && a.contains === Padding ? p.left + p.right : 0); } function viewHeight(view, height) { var a = view.autosize(), p = view.padding(); return height - (a && a.contains === Padding ? p.top + p.bottom : 0); } function initializeResize(view) { var s = view._signals, w = s[Width], h = s[Height], p = s[Padding]; function resetSize() { view._autosize = view._resize = 1; } // respond to width signal view._resizeWidth = view.add(null, _ => { view._width = _.size; view._viewWidth = viewWidth(view, _.size); resetSize(); }, { size: w }); // respond to height signal view._resizeHeight = view.add(null, _ => { view._height = _.size; view._viewHeight = viewHeight(view, _.size); resetSize(); }, { size: h }); // respond to padding signal const resizePadding = view.add(null, resetSize, { pad: p }); // set rank to run immediately after source signal view._resizeWidth.rank = w.rank + 1; view._resizeHeight.rank = h.rank + 1; resizePadding.rank = p.rank + 1; } function resizeView(viewWidth, viewHeight, width, height, origin, auto) { this.runAfter(view => { let rerun = 0; // reset autosize flag view._autosize = 0; // width value changed: update signal, skip resize op if (view.width() !== width) { rerun = 1; view.signal(Width, width, Skip); // set width, skip update calc view._resizeWidth.skip(true); // skip width resize handler } // height value changed: update signal, skip resize op if (view.height() !== height) { rerun = 1; view.signal(Height, height, Skip); // set height, skip update calc view._resizeHeight.skip(true); // skip height resize handler } // view width changed: update view property, set resize flag if (view._viewWidth !== viewWidth) { view._resize = 1; view._viewWidth = viewWidth; } // view height changed: update view property, set resize flag if (view._viewHeight !== viewHeight) { view._resize = 1; view._viewHeight = viewHeight; } // origin changed: update view property, set resize flag if (view._origin[0] !== origin[0] || view._origin[1] !== origin[1]) { view._resize = 1; view._origin = origin; } // run dataflow on width/height signal change if (rerun) view.run('enter'); if (auto) view.runAfter(v => v.resize()); }, false, 1); } /** * Get the current view state, consisting of signal values and/or data sets. * @param {object} [options] - Options flags indicating which state to export. * If unspecified, all signals and data sets will be exported. * @param {function(string, Operator):boolean} [options.signals] - Optional * predicate function for testing if a signal should be included in the * exported state. If unspecified, all signals will be included, except for * those named 'parent' or those which refer to a Transform value. * @param {function(string, object):boolean} [options.data] - Optional * predicate function for testing if a data set's input should be included * in the exported state. If unspecified, all data sets that have been * explicitly modified will be included. * @param {boolean} [options.recurse=true] - Flag indicating if the exported * state should recursively include state from group mark sub-contexts. * @return {object} - An object containing the exported state values. */ function getState(options) { return this._runtime.getState(options || { data: dataTest, signals: signalTest, recurse: true }); } function dataTest(name, data) { return data.modified && isArray(data.input.value) && !name.startsWith('_:vega:_'); } function signalTest(name, op) { return !(name === 'parent' || op instanceof transforms.proxy); } /** * Sets the current view state and updates the view by invoking run. * @param {object} state - A state object containing signal and/or * data set values, following the format used by the getState method. * @return {View} - This view instance. */ function setState(state) { this.runAsync(null, v => { v._trigger = false; v._runtime.setState(state); }, v => { v._trigger = true; }); return this; } function timer (callback, delay) { function tick(elapsed) { callback({ timestamp: Date.now(), elapsed: elapsed }); } this._timers.push(interval(tick, delay)); } function defaultTooltip (handler, event, item, value) { const el = handler.element(); if (el) el.setAttribute('title', formatTooltip(value)); } function formatTooltip(value) { return value == null ? '' : isArray(value) ? formatArray(value) : isObject(value) && !isDate(value) ? formatObject(value) : value + ''; } function formatObject(obj) { return Object.keys(obj).map(key => { const v = obj[key]; return key + ': ' + (isArray(v) ? formatArray(v) : formatValue(v)); }).join('\n'); } function formatArray(value) { return '[' + value.map(formatValue).join(', ') + ']'; } function formatValue(value) { return isArray(value) ? '[\u2026]' : isObject(value) && !isDate(value) ? '{\u2026}' : value; } function watchPixelRatio () { // based on https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio#monitoring_screen_resolution_or_zoom_level_changes if (this.renderer() === 'canvas' && this._renderer._canvas) { let remove = null; const updatePixelRatio = () => { if (remove != null) { remove(); } const media = matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`); media.addEventListener('change', updatePixelRatio); remove = () => { media.removeEventListener('change', updatePixelRatio); }; this._renderer._canvas.getContext('2d').pixelRatio = window.devicePixelRatio || 1; this._redraw = true; this._resize = 1; this.resize().runAsync(); }; updatePixelRatio(); } } /** * Create a new View instance from a Vega dataflow runtime specification. * The generated View will not immediately be ready for display. Callers * should also invoke the initialize method (e.g., to set the parent * DOM element in browser-based deployment) and then invoke the run * method to evaluate the dataflow graph. Rendering will automatically * be performed upon dataflow runs. * @constructor * @param {object} spec - The Vega dataflow runtime specification. */ function View(spec, options) { const view = this; options = options || {}; Dataflow.call(view); if (options.loader) view.loader(options.loader); if (options.logger) view.logger(options.logger); if (options.logLevel != null) view.logLevel(options.logLevel); if (options.locale || spec.locale) { const loc = extend({}, spec.locale, options.locale); view.locale(locale(loc.number, loc.time)); } view._el = null; view._elBind = null; view._renderType = options.renderer || RenderType.Canvas; view._scenegraph = new Scenegraph(); const root = view._scenegraph.root; // initialize renderer, handler and event management view._renderer = null; view._tooltip = options.tooltip || defaultTooltip, view._redraw = true; view._handler = new CanvasHandler().scene(root); view._globalCursor = false; view._preventDefault = false; view._timers = []; view._eventListeners = []; view._resizeListeners = []; // initialize event configuration view._eventConfig = initializeEventConfig(spec.eventConfig); view.globalCursor(view._eventConfig.globalCursor); // initialize dataflow graph const ctx = runtime(view, spec, options.expr); view._runtime = ctx; view._signals = ctx.signals; view._bind = (spec.bindings || []).map(_ => ({ state: null, param: extend({}, _) })); // initialize scenegraph if (ctx.root) ctx.root.set(root); root.source = ctx.data.root.input; view.pulse(ctx.data.root.input, view.changeset().insert(root.items)); // initialize view size view._width = view.width(); view._height = view.height(); view._viewWidth = viewWidth(view, view._width); view._viewHeight = viewHeight(view, view._height); view._origin = [0, 0]; view._resize = 0; view._autosize = 1; initializeResize(view); // initialize background color background(view); // initialize cursor cursor(view); // initialize view description view.description(spec.description); // initialize hover proessing, if requested if (options.hover) view.hover(); // initialize DOM container(s) and renderer if (options.container) view.initialize(options.container, options.bind); if (options.watchPixelRatio) view._watchPixelRatio(); } function lookupSignal(view, name) { return hasOwnProperty(view._signals, name) ? view._signals[name] : error('Unrecognized signal name: ' + stringValue(name)); } 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 removeOperatorListener(view, op, handler) { const h = findOperatorHandler(op, handler); if (h) op._targets.remove(h); return view; } inherits(View, Dataflow, { // -- DATAFLOW / RENDERING ---- async evaluate(encode, prerun, postrun) { // evaluate dataflow and prerun await Dataflow.prototype.evaluate.call(this, encode, prerun); // render as needed if (this._redraw || this._resize) { try { if (this._renderer) { if (this._resize) { this._resize = 0; resizeRenderer(this); } await this._renderer.renderAsync(this._scenegraph.root); } this._redraw = false; } catch (e) { this.error(e); } } // evaluate postrun if (postrun) asyncCallback(this, postrun); return this; }, dirty(item) { this._redraw = true; this._renderer && this._renderer.dirty(item); }, // -- GET / SET ---- description(text) { if (arguments.length) { const desc = text != null ? text + '' : null; if (desc !== this._desc) ariaLabel(this._el, this._desc = desc); return this; } return this._desc; }, container() { return this._el; }, scenegraph() { return this._scenegraph; }, origin() { return this._origin.slice(); }, signal(name, value, options) { const op = lookupSignal(this, name); return arguments.length === 1 ? op.value : this.update(op, value, options); }, width(_) { return arguments.length ? this.signal('width', _) : this.signal('width'); }, height(_) { return arguments.length ? this.signal('height', _) : this.signal('height'); }, padding(_) { return arguments.length ? this.signal('padding', padding(_)) : padding(this.signal('padding')); }, autosize(_) { return arguments.length ? this.signal('autosize', _) : this.signal('autosize'); }, background(_) { return arguments.length ? this.signal('background', _) : this.signal('background'); }, renderer(type) { if (!arguments.length) return this._renderType; if (!renderModule(type)) error('Unrecognized renderer type: ' + type); if (type !== this._renderType) { this._renderType = type; this._resetRenderer(); } return this; }, tooltip(handler) { if (!arguments.length) return this._tooltip; if (handler !== this._tooltip) { this._tooltip = handler; this._resetRenderer(); } return this; }, loader(loader) { if (!arguments.length) return this._loader; if (loader !== this._loader) { Dataflow.prototype.loader.call(this, loader); this._resetRenderer(); } return this; }, resize() { // set flag to perform autosize this._autosize = 1; // touch autosize signal to ensure top-level ViewLayout runs return this.touch(lookupSignal(this, 'autosize')); }, _resetRenderer() { if (this._renderer) { this._renderer = null; this.initialize(this._el, this._elBind); } }, // -- SIZING ---- _resizeView: resizeView, // -- EVENT HANDLING ---- addEventListener(type, handler, options) { let callback = handler; if (!(options && options.trap === false)) { // wrap callback in error handler callback = trap(this, handler); callback.raw = handler; } this._handler.on(type, callback); return this; }, removeEventListener(type, handler) { var handlers = this._handler.handlers(type), i = handlers.length, h, t; // search registered handlers, remove if match found while (--i >= 0) { t = handlers[i].type; h = handlers[i].handler; if (type === t && (handler === h || handler === h.raw)) { this._handler.off(t, h); break; } } return this; }, addResizeListener(handler) { const l = this._resizeListeners; if (!l.includes(handler)) { // add handler if it isn't already registered // note: error trapping handled elsewhere, so // no need to wrap handlers here l.push(handler); } return this; }, removeResizeListener(handler) { var l = this._resizeListeners, i = l.indexOf(handler); if (i >= 0) { l.splice(i, 1); } return this; }, addSignalListener(name, handler) { return addOperatorListener(this, name, lookupSignal(this, name), handler); }, removeSignalListener(name, handler) { return removeOperatorListener(this, lookupSignal(this, name), handler); }, addDataListener(name, handler) { return addOperatorListener(this, name, dataref(this, name).values, handler); }, removeDataListener(name, handler) { return removeOperatorListener(this, dataref(this, name).values, handler); }, globalCursor(_) { if (arguments.length) { if (this._globalCursor !== !!_) { const prev = setCursor(this, null); // clear previous cursor this._globalCursor = !!_; if (prev) setCursor(this, prev); // swap cursor } return this; } else { return this._globalCursor; } }, preventDefault(_) { if (arguments.length) { this._preventDefault = _; return this; } else { return this._preventDefault; } }, timer, events, finalize, hover, // -- DATA ---- data, change, insert, remove, // -- SCALES -- scale, // -- INITIALIZATION ---- initialize, // -- HEADLESS RENDERING ---- toImageURL: renderToImageURL, toCanvas: renderToCanvas, toSVG: renderToSVG, // -- SAVE / RESTORE STATE ---- getState, setState, // RE-RENDER ON ZOOM _watchPixelRatio: watchPixelRatio }); export { View }; //# sourceMappingURL=vega-view.js.map