'use strict'; /** * Licensed Materials - Property of IBM * IBM Cognos Products: BI Cloud (C) Copyright IBM Corp. 2014, 2020 * US Government Users Restricted Rights - Use, duplication or disclosure restricted by GSA ADP Schedule Contract with IBM Corp. * * @module dashboard/render/VisView * @see VisView */ define(['jquery', 'underscore', 'doT', '../../lib/@waca/core-client/js/core-client/ui/core/View', './filter/FilterIndicator', './info/InfoIndicator', '../../widgets/livewidget/nls/StringResources', '../../lib/@waca/core-client/js/core-client/utils/Utils', './refreshtimer/RefreshTimerIndicator', 'text!./templates/EmptyVisualization.template', './forecastIndicator/ForecastIndicator', './VisTabs', '../../lib/@waca/core-client/js/core-client/utils/BrowserUtils', '../../widgets/livewidget/util/VisUtil', '../../lib/@waca/dashboard-common/dist/utils/MemUtil'], function ($, _, dot, View, FilterIndicator, InfoIndicator, StringResources, Utils, RefreshTimerIndicator, EmptyVisualizationTemplate, ForecastIndicator, VisTabs, BrowserUtils, VisUtil, MemUtil) { //Track sub-views created to help with cleanup var VisViewSubViews = ['filterIndicator', 'refreshTimerIndicator', 'conditionalViewIndicator', 'infoIndicator']; /** * VisView is the base class of all Visualization views * such as RaveView, GridView etc. */ var VisView = View.extend({ //The animation speed for all operations if no animation speed is set in properties. DEFAULT_ANIMATION_SPEED: 900, //The animation type (if none is set, default is currently to fadein). ANIMATION_TYPES: { NONE: 'none', DEFAULT: 'default', FADEIN: 'fadein', TRANSITION: 'transition', GROW: 'grow', REVEAL: 'reveal', BIGDATA: 'bigdata' }, init: function init(options) { VisView.inherited('init', this, arguments); _.extend(this, arguments[0]); this.noRotate = true; this.isMaximizable = true; this.logger = options.logger; this.ownerWidget = options.ownerWidget; this.visualization = this.ownerWidget.getVisualizationApi(); this.transactionApi = this.dashboardApi.getFeature('Transaction'); this.internalVisDefinitions = this.dashboardApi.getFeature('VisDefinitions.internal'); this.content = options.ownerWidget.content; this._processTemplateString(); if (!options.proxiedView && !this._viewManagesOwnQuery()) { this._initIndicators(); } this.visEl = this.el.firstChild ? this.el.firstChild : this.el; // create the visualization tabs (if supported) this.createTabs(); //When set by an event handler (aka filter), this value controls the animation speed //for views that otherwise don't animate (but do support it). //If the definition already has animation, it will NOT override its value. this.overrideDefaultAnimationSpeed = -1; //This setting controls Non-VIPR visualizations allowing them to fade-in/fade-out during rendering. //VIPR visualizations use effects which are separate from this setting. //By default, Non-VIPR animation is off: (new for Endor) as it causes needless flashing and makes visualizaions appear slower. this.animationType = this.ANIMATION_TYPES.NONE; this.animationSpeed = this.DEFAULT_ANIMATION_SPEED; //The 'renderingNewData' state (maintained by the rendersequnce) is relevant in the context of the view's render() method. //It means that, for a particular view.render() call, new data is being rendered. this._renderingNewData = false; if (!options.proxiedView && !this._viewManagesOwnQuery()) { this.createView(); } if (!options.proxiedView) { this._addPropertyHandler(); } }, createTabs: function createTabs() { var options = {}; // reuse the existing DIV if already exists if (this.ownerWidget && this.ownerWidget.$el) { var $tabs = this.ownerWidget.$el.find('.visTabsView'); options.el = $tabs.length ? $tabs[0] : null; } this._tabs = new VisTabs(options); // keep it hidden until render this._tabs.hide(); }, placeAt: function placeAt(element) { this.$el.prependTo(element); // insert view as the first element of the content container //if tab view exists, insert tabs as the first element if (this._tabs) { this._tabs.placeAt(element); } }, setInExpandedMode: function setInExpandedMode() { var conditionalViewIndicator = this.content.getFeature('ConditionalViewIndicator'); if (conditionalViewIndicator) { conditionalViewIndicator.setInExpandedMode(); } }, onRestore: function onRestore() { var conditionalViewIndicator = this.content.getFeature('ConditionalViewIndicator'); if (conditionalViewIndicator) { conditionalViewIndicator.onRestore(); } }, /** * @returns if this specific view supports annotations. Returns false by default, expected to be overriden. */ _doesViewSupportSmartAnnotations: function _doesViewSupportSmartAnnotations() { return false; }, // to be implemented by chilren class doesVisPropertyMatchExpected: function doesVisPropertyMatchExpected() { return false; }, // to be implemented by chilren class canApplyAutoBin: function canApplyAutoBin() { return false; }, // to be implemented by chilren class getBinningConfig: function getBinningConfig() { return undefined; }, /* * Reads the filter indicator config spec, and instantiates a filter indicator if applicable. */ _initFilterIndicator: function _initFilterIndicator() { var filterSpec = this.getFilterIndicatorSpec(); if (filterSpec && (filterSpec.localFilters || filterSpec.globalFilters || filterSpec.localTopBottoms || filterSpec.drillState)) { this.filterIndicator = new FilterIndicator(this.visModel, this.visualization, this.transactionApi, this.getController(), filterSpec, this.logger); this.ownerWidget.addIcon(this.filterIndicator.$filter, 'filterIcon'); } }, _initIndicators: function _initIndicators() { var _this = this; var visIndicators = this.content.getFeature('VisIndicators'); if (visIndicators && this.ownerWidget && this.ownerWidget.$el) { this._initRefreshIndicator(); this._initFilterIndicator(); this._initInfoIndicator(); this._initForecastingIndicator(); var indicators = visIndicators.getIndicatorList(); _.each(indicators, function (indicator) { _this.ownerWidget.addIcon(indicator.icon, indicator.name); }); } }, _initForecastingIndicator: function _initForecastingIndicator() { this.forecastIndicator = new ForecastIndicator({ visModel: this.visModel, ownerWidget: this.ownerWidget, logger: this.logger, supportsForecasts: true, ownerView: this, visAPI: this.ownerWidget.visAPI, content: this.content }); this.ownerWidget.addIcon(this.forecastIndicator.getIndicator(), 'forecastIcon'); }, _initInfoIndicator: function _initInfoIndicator() { this.infoIndicator = new InfoIndicator({ visModel: this.visModel, ownerWidget: this.ownerWidget }); this.ownerWidget.addIcon(this.infoIndicator.$el, 'infoIconDiv'); }, _initRefreshIndicator: function _initRefreshIndicator() { this.refreshTimerIndicator = new RefreshTimerIndicator(this.ownerWidget); this.ownerWidget.addIcon(this.refreshTimerIndicator.$icon, 'timerIcon'); }, /* * Indicates whether to render filter flyouts */ getFilterIndicatorSpec: function getFilterIndicatorSpec() { return { localFilters: true, globalFilters: true, localTopbottoms: true, drillState: true }; }, getController: function getController() { return this.content && this.content.getFeature('InteractivityController.deprecated'); }, getSelector: function getSelector() { return this.content.getFeature('DataPointSelections.deprecated'); }, /* * Indicates whether to render topbottom flyouts. Unlike filter, topbottom only applys to the current viz there is no global */ /** * Render Sequence - override this method when needed by a specific view type. * @returns promise - By default, views don't have controls....return a resolved promise with an empty object. */ whenVisControlReady: function whenVisControlReady() { return this._noOpRenderStep('base class whenVisControlReady called!'); }, /** * Render Sequence - override this method when needed by a specific view type. * @returns promise - By default, views don't have visSpecs....return a resolved promise with an empty object. */ whenVisSpecReady: function whenVisSpecReady() { return this._noOpRenderStep('base class whenVisSpecReady called!'); }, /** * @returns {boolean} true if this specific view supports annotations. Returns * false by default. Expected to be overriden */ supportsAnnotations: function supportsAnnotations() { return false; }, /** * @returns {boolean} true if this specific view supports annotations. Returns * false by default. Expected to be overriden */ supportsForecasts: function supportsForecasts() { return false; }, /** * Called before data is requested. Use this to clean up infoIndicator, etc. */ preDataReady: function preDataReady() { // Reset any warnings/errors in the info indicator as we are starting fresh if (this.infoIndicator) { this.infoIndicator.clearMessagesWithIds(['visualization_notifications', 'moreDataIndicator', 'autobinSuggestion', 'data_notifications']); } }, _hasMappedSlots: function _hasMappedSlots() { return this.visualization.getSlots().getMappedSlotList().length !== 0; }, /** * Render Sequence - override this method when needed by a specific view type. * @returns promise - By default, make a query for the data and do not index the results (ie: RaveView is an exception). */ whenDataReady: function whenDataReady(renderContext) { var _this2 = this; var result = void 0; // A resize event is forced when the layout base view is initialized. This may cause a request // to render the visualization before the visControl is created. Guard against this. if (this._hasMappedSlots() && this.canExecuteQuery() && !this.hasUnavailableMetadataColumns() && !this.hasMissingFilters()) { renderContext.isDevInstall = this.ownerWidget.getDashboardApi().isDevInstall; result = this._executeQueries(renderContext).then(function (retData) { _this2.dashboardApi.triggerDashboardEvent('synchronize:pageContextFilters', { synchDataFilterEntries: retData.synchDataFilterEntries }); if (_this2.filterIndicator) { _this2.filterIndicator.setSynchronizeDataFilters(retData.synchDataFilterEntries); _this2.filterIndicator.update(); } _this2._updateInfoIndicator(retData, renderContext); return { data: retData, sameQueryData: _this2.isSameQueryData(renderContext, retData) }; }); } else { result = Promise.resolve({ data: {} }); } return result; }, _updateInfoIndicator: function _updateInfoIndicator(queryResults, renderContext) { var hasMoreData = void 0; var queryThreshold = void 0; var warnings = void 0; if (renderContext.useAPI) { var queryId = _.find(queryResults.getQueryResultIdList(), function (id) { return queryResults.getResult(id).hasMoreData(); }); hasMoreData = queryId !== undefined; queryThreshold = hasMoreData && queryResults.getResult(queryId).getRowCount(); warnings = queryResults.getWarningList(); } else { hasMoreData = queryResults.hasMoreData; queryThreshold = queryResults.queryThreshold; } if (this.infoIndicator) { var infoItems = []; // @todo: utilize the QueryResultsAPI.getWarningList if (hasMoreData) { // add data clipping if (queryThreshold) { infoItems.push({ id: 'moreDataIndicator', label: StringResources.get('moreDataIndicator', { threshold: queryThreshold }) }); } // check auto-binning: 1) is it supported and 2) not already binned if (this.canApplyAutoBin() && !this.hasBinnedDataItems()) { infoItems.push({ id: 'autobinSuggestion', label: StringResources.get('autobinSuggestion'), isSubMessage: true }); } } if (warnings && warnings.length) { warnings.forEach(function (warning) { infoItems.push({ id: warning.resourceId, label: StringResources.get(warning.resourceId, warning.params), isSubMessage: !!warning.isSubMessage }); }); } this.infoIndicator.addInfo([{ id: 'data_notifications', label: StringResources.get('dataNotifications'), items: infoItems }]); } }, _executeQueries: function _executeQueries(renderContext) { if (renderContext.useAPI) { var queryExecution = this.content.getFeature('DataQueryExecution'); if (renderContext.extraInfo && renderContext.extraInfo.dataQueryParams) { queryExecution.addRequestOptions(renderContext.extraInfo.dataQueryParams); } return queryExecution.executeQueries(); } else { return this.visModel.whenQueryResultsReady(renderContext); } }, /** * Render Sequence - override this method when needed by a specific view type. * @returns {Promise} */ whenSetDataReady: function whenSetDataReady() /* renderContext */{ return Promise.resolve(true); }, isSameQueryData: function isSameQueryData() { var renderContext = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var queryResult = arguments[1]; // TODO clean logic when QueryAPI is live. if (renderContext.sameQueryData === undefined) { var currTag = queryResult && queryResult.getCacheValidateTag && queryResult.getCacheValidateTag(); renderContext.sameQueryData = this._previousTag && this._previousTag === currTag; this._previousTag = currTag; } return !!(renderContext.sameQueryData || renderContext.extraInfo && renderContext.extraInfo.annotationRequest); }, /* For Subclass to implement */ updateInfoIndicator: function updateInfoIndicator(data) { if (this.infoIndicator) { this.infoIndicator.update(data); } }, /** * The 'renderingNewData' state (maintained by the renderSequence) is relevant in the context of the view's render() method. * It means that, for a particular view.render() call, new data is being rendered. * (for example, typically, render following a resize would not be renderingNewData BUT, * If a resize occurs quickly after a render, 2 RenderSequence render() calls could result in one view:render() call * (effecively the 2 renders are "bundled into 1" from the perspective of the view so we are renderingNewData) * @param {boolean} flag - The renderingNewData value is maintained by the render sequence as follows: * true: when the data task has executed a query and has new data. * false: when the render task has completed a render */ setRenderingNewData: function setRenderingNewData(flag) { this._renderingNewData = flag; }, /** * @returns the state of the renderingNewData flag. This call makes sense in the context of a particular view.render(). */ getRenderingNewData: function getRenderingNewData() { return this._renderingNewData; }, /** * @returns true if all of the required slots are mapped and this visualization can be rendered. */ isMappingComplete: function isMappingComplete() { return this.visualization.getSlots().isMappingComplete(); }, /** * Determine whether the query can be executed. * Generally the query shall execute when all required mapping is complete. * However some applications may require to execute the query regardless of the slot mapping state. * The config should only affect the behaviour of the query and should have no impact on the actual render * of the visualization. The render should continue to render only when isMappingComplete is true. */ canExecuteQuery: function canExecuteQuery() { return this.dashboardApi.getConfiguration('forceQuery') === true || this.isMappingComplete(); }, isRenderWithoutCompletingMapping: function isRenderWithoutCompletingMapping() { var definition = this.visualization.getDefinition(); return definition.getProperty('renderWithoutCompletingMapping') === true; }, hasUnavailableMetadataColumns: function hasUnavailableMetadataColumns() { return this.visualization.hasUnavailableMetadataColumns(); }, /** * Render Sequence - override this method when needed by a specific view type. * @returns promise - By default, views don't have visSpecs....return a resolved promise with an empty object. */ whenHighlighterReady: function whenHighlighterReady() { return this._noOpRenderStep('base class whenHighlighterReady called!'); }, /** * Creates an object consumable by our info indicator * @param {object} id id of our info, needs a matching entry in our nls entries * @param {Array} items * @param {String} item.id * @param {String} item.caption */ _getInfoIndicatorEntries: function _getInfoIndicatorEntries(id, items) { return [{ id: id, label: StringResources.get(id), items: _.map(items, function (item) { return { id: item.id, label: item.caption }; }) }]; }, _setInfoIndicatorWarningsAndErrors: function _setInfoIndicatorWarningsAndErrors(data) { if (this.infoIndicator && data) { var errorId = 'smart_errors'; var warningId = 'smart_warnings'; var warnings = data.getWarnings(); var errors = data.getErrors(); this.infoIndicator.addInfo(this._getInfoIndicatorEntries(warningId, warnings)); this.infoIndicator.addInfo(this._getInfoIndicatorEntries(errorId, errors)); } }, clearInfoIndicator: function clearInfoIndicator() { // Reset any warnings/errors in the info indicator as we are starting fresh if (this.infoIndicator) { //if our mapping isn't complete, just clear out the info indicator. if (!this.isMappingComplete()) { this.infoIndicator.reset(); } else { for (var _len = arguments.length, msgId = Array(_len), _key = 0; _key < _len; _key++) { msgId[_key] = arguments[_key]; } this.infoIndicator.clearMessagesWithIds(msgId); } } }, clearAnnotationErrorsAndWarnings: function clearAnnotationErrorsAndWarnings() { this.clearInfoIndicator('smart_errors', 'smart_warnings', 'smart_exec_support_warnings', 'annotation_service_error'); }, /** * Render Sequence - override this method when needed by a specific view type. * @returns promise - By default, views don't have visSpecs....return a resolved promise with an empty object. */ whenAnnotatedResultsReady: function whenAnnotatedResultsReady(renderContext) { var _this3 = this; if (this.isMappingComplete() && !this.hasUnavailableMetadataColumns() && (this._checkAllSupportsAnnotations() || this._checkAllSupportsForecasts())) { return this.visModel.whenAnnotatedResultsReady(renderContext).then(function (data) { _this3._setInfoIndicatorWarningsAndErrors(data); return data; }); } else { return Promise.resolve(); } }, addUnavailableAnnotationServiceErrorMessageToIndicator: function addUnavailableAnnotationServiceErrorMessageToIndicator() { if (this.infoIndicator) { this.infoIndicator.addInfo([{ id: 'annotation_service_error', label: StringResources.get('smart_errors'), items: [{ id: 'suggestions', label: StringResources.get('smart_annotation_insight_unavailable') }] }]); } }, /** * Render Sequence - override this method when needed by a specific view type. * @returns {object} promise */ whenPredictSuggestionsReady: function whenPredictSuggestionsReady(renderContext) { var _this4 = this; this.clearInfoIndicator('annotation_service_error', 'suggestions'); // Clear the messages so that we aren't showing stale info this.visModel.clearInsightsIndicatorMessages(); if (this.content) { this.content.getFeature('Forecast').clearForecastIndicatorMessages(); } var supportsAnnotations = this._checkAllSupportsAnnotations(); if (!supportsAnnotations) { this.visModel.resetAnnotations(); } if (this._hasMappedSlots() && this.isMappingComplete()) { return this.visModel.whenPredictIsReady(renderContext, supportsAnnotations, this._checkAllSupportsForecasts()).catch(function (error) { _this4.addUnavailableAnnotationServiceErrorMessageToIndicator(); _this4.logger.error('An error occurred while waiting for the annotation suggestions to be ready', error, _this4.visModel); }); } return Promise.resolve(); }, _checkAllSupportsAnnotations: function _checkAllSupportsAnnotations() { return this.supportsAnnotations() && this._viewSupportsAnnotations(); }, //We enable annotations for preview launched from Driver analysis Viz key drivers, //Except that, for other previews, we disable annotations. _viewSupportsAnnotations: function _viewSupportsAnnotations() { if (this.ownerWidget.isPreview) { return this.ownerWidget.isPredictPreview === true; } return true; }, _checkAllSupportsForecasts: function _checkAllSupportsForecasts() { return this.supportsForecasts() && this._viewSupportsForecasts(); }, _viewManagesOwnQuery: function _viewManagesOwnQuery() { if (this.ownerWidget.managesOwnQueries) { return this.ownerWidget.managesOwnQueries === true; } return false; }, //We enable forecasts for preview launched from Driver analysis Viz key drivers, //Except that, for other previews, we disable annotations. _viewSupportsForecasts: function _viewSupportsForecasts() { if (this.ownerWidget.isPreview) { return this.ownerWidget.isPredictPreview === true; } return true; }, /** * Render Sequence: * Each step in the render sequence returns a promise. * For any view, or any step, this method can be used to return a resolved promise with an empty object */ _noOpRenderStep: function _noOpRenderStep() { //By default, VisViews have no underlying VisControl...return an empty, non-null object. return Promise.resolve({}); }, _addPropertyHandler: function _addPropertyHandler() { this.ownerWidget.model.on('change:properties', this.onChangeProperties, this); this.ownerWidget.model.on('change:fillColor', this.onChangeProperties, this); }, /** * Set up the View and standard event handlers on the VisModel. */ createView: function createView() { if (!this.visModel) { throw 'Invalid VisModel reference'; } //Subscribe to model events this.visModelEvents = { 'change:annotations': this.onChangeAnnotations, 'change:pagecontext': this.onChangePageContext, //pageContext changes should be processed as filters. 'change:refreshTimer': this.toggleRefreshTimerIndicator }; this.ownerWidget.model.on('change:searchFilters', this.onChangeLocalFilter, this); this.ownerWidget.model.on('change:localFilters', this.onChangeLocalFilter, this); this.ownerWidget.model.on('change:conditions', this.onChangeConditions, this); this.ownerWidget.model.on('change:possibleKeyDrivers', this.onChangePossibleKeyDrivers, this); for (var key in this.visModelEvents) { if (this.visModelEvents.hasOwnProperty(key)) { this.visModel.on(key, this.visModelEvents[key], this); } } if (this.filterIndicator) { this.filterIndicator.initEvents(); } if (this.refreshTimerIndicator) { this.refreshTimerIndicator.initEvents(); } if (this.forecastIndicator) { this.forecastIndicator.initEvents(this.supportsForecasts()); } var content = this.ownerWidget.content; content && content.on('all', this.onAllContentChanges, this); var visDefinitions = this.dashboardApi.getFeature('VisDefinitions'); visDefinitions.on('refresh:definition', this.onRefreshVisDefinition, this); visDefinitions.on('refresh:all', this.onRefreshAllVisDefinition, this); var colorsService = this.dashboardApi.getFeature('Colors'); colorsService.on('theme:changed', this.onChangeProperties, this); }, /** * Event handler when possible key drivers are changed inside vis model */ onChangePossibleKeyDrivers: function onChangePossibleKeyDrivers() { var event = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; if (this._isViewSlotsEmpty()) { return this.ownerWidget._rebuildVis(event); } var reRenderOptions = { refresh: { predictSuggestions: true, keyDrivers: true, data: true } }; if (this.filterIndicator) { this.filterIndicator.update(); } this.appendExtraInfoToRenderOptions(reRenderOptions, event); if (this.ownerWidget.isVisible()) { this._reRender(reRenderOptions); } else { this.ownerWidget.setReRenderOnShow(reRenderOptions); } }, onRefreshAllVisDefinition: function onRefreshAllVisDefinition() { var visDefinition = this.visualization.getDefinition(); if (visDefinition.getState().getError() || this.content.getFeature('state').getError()) { this._reRender({ refreshAll: true }); } }, onRefreshVisDefinition: function onRefreshVisDefinition(event) { var visDefinition = this.visualization.getDefinition(); if (visDefinition && event && event.info && visDefinition.getId() === event.info.id) { this._reRender({ refreshAll: true }); } }, onAllContentChanges: function onAllContentChanges(e) { if (e && e.info && e.info.refresh) { var renderOptions = { refresh: _.extend({}, e.info.refresh), extraInfo: { payloadData: { transactionToken: e.transactionToken } } }; if (renderOptions.refresh.all) { // TODO: refreshAll should be changed into 'refresh.all' to be consistent with the refresh syntax renderOptions.refreshAll = renderOptions.refresh.all; } // Type change // highlighting is included because this event may have // been triggered from a failure handler. // ensure the remaining rendering sequence is covered // var reRenderOptions = { // refresh: { // visSpec: true, // data: true, // highlighter: true, // predictSuggestions: true // } // }; // //If currently rendering icon view, ensure the proper control is loaded. // reRenderOptions.refresh.visControl = true; // this._reRender(this.appendExtraInfoToRenderOptions(reRenderOptions, event)); // this.updateConditionalViewIndicator(); // Todo: once we removed the logic for refresh properties pane upon re-render, this should be removed too // UndoRedo action will update the model but the old properties UI doesn't have state management, so we need to do a refresh for this scenario var isUndoRedo = e.context && e.context.undoRedo && e.info.featureName !== 'ConditionalFormatting'; renderOptions.refresh.propertiesPane = this._refreshPropertiesPane(renderOptions, isUndoRedo); this._reRender(renderOptions); } }, _refreshPropertiesPane: function _refreshPropertiesPane(renderOptions, isUndoRedo) { return !this._isLocalViz() || renderOptions.refresh.propertiesPane || !this._isNewConditionalFormattingEnabled() || !!isUndoRedo; }, _isLocalViz: function _isLocalViz() { var localVizType = ['KPI', 'Singleton', 'Hierarchy', 'Crosstab', 'List', 'DataPlayer']; var visType = this.content.getFeature('Visualization').getType(); return _.contains(localVizType, visType); }, _isNewConditionalFormattingEnabled: function _isNewConditionalFormattingEnabled() { var cf_kpi = !this.dashboardApi.getGlassCoreSvc('.FeatureChecker').checkValue('dashboard', 'condFormat', 'disabled'); var cf_xtab = !this.dashboardApi.getGlassCoreSvc('.FeatureChecker').checkValue('dashboard', 'xtabcondFormat', 'disabled'); return cf_kpi || cf_xtab; }, updateSubViews: function updateSubViews() { this.updateConditionalViewIndicator(); }, /** * If this view has a template string, make its content a child of the root "el". * If the template includes "data-attach-point" attributes, augment the view class * with them. * eg: * *
*
* * * would extend the VisView with members sliderNode=div.x, playerButtonNode=div.y */ _processTemplateString: function _processTemplateString() { if (this.templateString) { //Populate the view with the template if one has been defined. var outTemplate = this.dotTemplate(this.templateString); if (!outTemplate) { outTemplate = this.templateString; } this.$el.html(outTemplate); var dataAttachPoints = this.el.querySelectorAll('div[data-attach-point]'); var oAttachPoints = {}; _.each(dataAttachPoints, function (dataAttachPoint) { oAttachPoints[dataAttachPoint.getAttribute('data-attach-point')] = dataAttachPoint; }); _.extend(this, oAttachPoints); } }, /** * Resize the el for this view to the specified bounds * @param bounds in form left, top, width, height. */ resize: function resize(bounds) { if (bounds) { // capture widget height, if zero or undefined then get parent's height var height = bounds.height || this.$el.parent().height(); // if we have tabs, we must compensate for the space it takes within the widget if the mapping is complete if (this._tabs && this._tabs.getTabsCount() > 0 && this.ownerWidget.allowShowTabs && this.ownerWidget.allowShowTabs() && this.isMappingComplete()) { height = height - this._tabs.$el.height(); } var fSetBound = function fSetBound(style, styleAttr, val) { // Setting style may be expensive, do it only when necessary. if (val !== undefined && val !== parseInt(style[styleAttr], 10)) { style[styleAttr] = val + 'px'; } }; var style = this.el.style; fSetBound(style, 'left', bounds.left); fSetBound(style, 'top', bounds.top); fSetBound(style, 'width', bounds.width); fSetBound(style, 'height', height); } }, /** * Remove this view...cleanup any event handlers set up * and perform base class cleanup. */ // @override remove: function remove() { var _this5 = this; var finalRemove = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true; this.removeIconView(); VisView.inherited('remove', this, arguments); // todo livewidget_cleanup -- it is weird that we pass a finalRemove // It seems to be used to delayed the destruction of a vipr widget // but we could handle this in a better way VisViewSubViews.forEach(function (view) { _this5[view] && _this5[view].remove(); _this5[view] = null; }); // external events for (var key in this.visModelEvents) { if (this.visModelEvents.hasOwnProperty(key)) { this.visModel.off(key, this.visModelEvents[key], this); } } this.ownerWidget.model.off('change:searchFilters', this.onChangeLocalFilter, this); this.ownerWidget.model.off('change:localFilters', this.onChangeLocalFilter, this); this.ownerWidget.model.off('change:properties', this.onChangeProperties, this); this.ownerWidget.model.off('change:fillColor', this.onChangeProperties, this); this.ownerWidget.model.off('change:conditions', this.onChangeConditions, this); this.ownerWidget.model.off('change:possibleKeyDrivers', this.onChangePossibleKeyDrivers, this); if (this._tabs) { this._tabs.remove(); this._tabs = null; } var content = this.ownerWidget.content; content && content.off('all', this.onAllContentChanges, this); var visDefinitions = this.dashboardApi.getFeature('VisDefinitions'); visDefinitions.off('refresh:definition', this.onRefreshVisDefinition, this); visDefinitions.off('refresh:all', this.onRefreshAllVisDefinition, this); var colorsService = this.dashboardApi.getFeature('Colors'); colorsService.off('theme:changed', this.onChangeProperties, this); if (finalRemove) { MemUtil.destroy(this); } }, onChangePageContext: function onChangePageContext(event) { var _this6 = this; return this.onChangeFilter(event).catch(function (e) { var LOG_LEVEL = e && e.message === 'dwErrorStaleRequest' ? 'debug' : 'error'; _this6.logger[LOG_LEVEL](e); }); }, _rebuildVis: function _rebuildVis(event, options) { this._rebuildPending = true; var transactionToken = options && options.extraInfo && options.extraInfo.payloadData && options.extraInfo.payloadData.transactionToken; return this.transactionApi.registerTransactionHandler(transactionToken || {}, this.ownerWidget.id + '_rebuildvis', function () { var _this7 = this; this.ownerWidget._rebuildVis(event).then(function () { _this7._rebuildPending = false; }); }.bind(this), options); }, _getRenderTransaction: function _getRenderTransaction(renderOptions) { var transaction = renderOptions && renderOptions.extraInfo && renderOptions.extraInfo.payloadData && renderOptions.extraInfo.payloadData.transactionToken; return !transaction || transaction.transactionId ? transaction : null; }, _reRender: function _reRender(options) { if (!this._rebuildPending) { if (this.ownerWidget.isVisible()) { var transactionToken = this._getRenderTransaction(options); // Only attempt to render requests with: // 1. no transaction token // 2. valid transaction token if (!transactionToken || this.transactionApi.isValidTransaction(transactionToken)) { return this.transactionApi.registerTransactionHandler(transactionToken || {}, this.ownerWidget.id, function (opt) { if (this.ownerWidget) { var visApi = this.ownerWidget.getAPI().getVisApi(); return visApi.getRenderSequence().reRender(this._mergeOptions(opt)); } return Promise.resolve(); }.bind(this), options); } } else { this.ownerWidget.setReRenderOnShow(options); } } return Promise.resolve(); }, /** * @description This function will take the first object in the array and merge the 'refresh' parameters from the other objects into it * @param {Array} of render options * @returns {Object} the merged options object or the first object if there are no others to merge */ _mergeOptions: function _mergeOptions(options) { var mergedOptions = options[0]; var length = options.length; if (length > 1) { for (var i = 1; i < length; i++) { mergedOptions.refresh = this._mergeRefreshOptions(mergedOptions.refresh, options[i].refresh); if (options[i].refreshAll) { mergedOptions.refreshAll = options[i].refreshAll; } } } if (mergedOptions.refresh && mergedOptions.refresh.data && mergedOptions.refresh.dataIfQueryChanged) { // data refresh trumps dataIfQueryChanged delete mergedOptions.refresh.dataIfQueryChanged; } return mergedOptions; }, _mergeRefreshOptions: function _mergeRefreshOptions(refreshOptions, options) { Object.keys(options).forEach(function (key) { if (!refreshOptions.hasOwnProperty(key)) { refreshOptions[key] = options[key]; } else { refreshOptions[key] = refreshOptions[key] || options[key]; } }); return refreshOptions; }, /** * Called when a filter on the VisModel changes. * By default.... * * re-render the view when the event sender is not the same as the receiver * re-render the selections */ // HANDLER: visModel.on('change:filters') onChangeFilter: function onChangeFilter(event) { var _this8 = this; event = event || {}; var changeEvent = event.changeEvent; var transactionToken = this.transactionApi.startTransaction(changeEvent && changeEvent.data && changeEvent.data.transactionToken); var sender = event && event.sender; return VisUtil.validateVisDefinition(this.content, this.dashboardApi, { visId: this.ownerWidget.model.visId }).then(function (isValid) { if (isValid) { return _this8._queryChanged().then(function (result) { result = result || {}; var refreshData = result.isRenderNeeded; var extraInfo = void 0; var renderOptions = { refresh: { dataIfQueryChanged: refreshData, annotation: true } }; if (_this8.filterIndicator) { _this8.filterIndicator.setSynchronizeDataFilters(result.synchDataFilterEntries); _this8.filterIndicator.update(); } if (!refreshData) { // optimization to avoid making unnecessary render requests if (!changeEvent || changeEvent.isInScope(_this8.ownerWidget.getScope(), _this8.ownerWidget.getEventGroupId())) { if (changeEvent && !changeEvent.brushingChanged() && _.every(changeEvent.getItems(), function (item) { return item.getValueCount() === 0; })) { // If the filter doesn't have any value and it is not a brushing event, do not rerender. return; } // @todo is this necessary? if (result.context) { result.context.visAPI = _this8.visAPI; } } else { _this8.visModel.renderComplete(); return; } // ensure we don't introduce unnecessary animations upon re-render with no data change extraInfo = { entryDuration: 'zero', payloadData: { transactionToken: transactionToken } }; } _this8._reRender(_this8.appendExtraInfoToRenderOptions(renderOptions, event, extraInfo)).then(function () { if (!refreshData) { _this8.dashboardApi.triggerDashboardEvent('dataInVis:selected', { payloadData: _this8.ownerWidget, sender: _this8.ownerWidget.id }); var isOriginatorOfSelection = event.changeEvent && event.changeEvent.senderMatches(_this8.ownerWidget.id); //isOriginator means this widget is the originator of the selection change (not just one that is responding to the selection change). _this8.ownerWidget.trigger('visevent:selectionchanged', { sender: _this8.ownerWidget.id, isSelectionOriginator: isOriginatorOfSelection }); _this8.renderFocused(!sender || sender === _this8.ownerWidget.id); } }); }); } }).finally(function () { _this8.transactionApi.endTransaction(transactionToken); }); }, _queryChanged: function _queryChanged() { // TODO Wrap to async result when use new query API. Need to be cleaned once switch to query api. if (this.ownerWidget.useNewQueryApi()) { var internalQueryExecution = this.content.getFeature('DataQueryExecution.internal'); return internalQueryExecution.queryChanged().then(function (changed) { return { isRenderNeeded: changed }; }); } else { return this.visModel.queryChanged(); } }, toggleRefreshTimerIndicator: function toggleRefreshTimerIndicator(event) { if (this.refreshTimerIndicator.$icon) { if (event.autoRefresh) { this.refreshTimerIndicator.$icon.removeClass('dataWidgetTimersNone'); } else { this.refreshTimerIndicator.$icon.addClass('dataWidgetTimersNone'); } } }, /** * Called when a filter that only affects this widget changes. * Such as from the filter dialog. * * re-render the selections */ onChangeLocalFilter: function onChangeLocalFilter(event) { var renderOptions = { refresh: { data: true, annotation: true } }; this.ownerWidget.updateMissingFilters(); /* vis isn't tied to data anymore*/ if (this._isViewSlotsEmpty()) { this._rebuildVis(event, this.appendExtraInfoToRenderOptions(renderOptions, event)); return; } this.visModel.setPendingFilters(false); //cancel any pending filters var sender = event && event.sender; this._reRender(this.appendExtraInfoToRenderOptions(renderOptions, event)); this.renderFocused(!sender || sender === this.ownerWidget.id); if (this.filterIndicator) { this.filterIndicator.update(); } }, onChangePendingFilter: function onChangePendingFilter() { this.visModel.setPendingFilters(true); }, /** * Slots include vis slots and local filters slots */ _isViewSlotsEmpty: function _isViewSlotsEmpty() { return !this.visualization.getSlots().getMappedSlotList().length && this.visModel.getLocalFilters().isEmpty(); }, _annotationHasChanged: function _annotationHasChanged(event) { var prev = event && event.changeEvent && event.changeEvent.prevValue; var curr = event && event.changeEvent && event.changeEvent.value; if (prev && prev.selectedAnnotations && prev.selectedAnnotations.length || curr && curr.selectedAnnotations && curr.selectedAnnotations.length) { return true; } else { return false; } }, onChangeAnnotations: function onChangeAnnotations(event) { if (this._annotationHasChanged(event)) { var renderOptions = { refresh: { dataIfQueryChanged: true, annotation: true }, extraInfo: { annotationRequest: true } }; return this._reRender(this.appendExtraInfoToRenderOptions(renderOptions, event)); } return Promise.resolve(); }, onVisible: function onVisible() {}, isVisible: function isVisible() { return this.$el.parent().is(':visible'); }, /** * Called when a property (like a font or a colour) changes. * By default.... * re-render the view * re-render the selections * Set queryManager and render options if one of the properties requiring data refresh have changed (currently maintainAxisScales) * @param event - an event containing the single property from the property collection that changed (eg: 'showAxisTitles'). */ // HANDLER: visModel.on('change:property') onChangeProperties: function onChangeProperties(modelEvent) { var event = { name: 'properties', value: modelEvent && modelEvent.value, sender: null, changeEvent: modelEvent, refreshData: modelEvent && modelEvent.origCollectionEvent && modelEvent.origCollectionEvent.dataRefresh }; var renderOptions = { refresh: {} }; if (event && event.refreshData || renderOptions.refresh.data) { if (_.isEmpty(renderOptions.refresh)) { renderOptions.refresh.data = true; } } // re-render the visualization var reRenderOptions = this.appendExtraInfoToRenderOptions(renderOptions, event); this._reRender(reRenderOptions); }, /** * Called when the conditional formatting changes */ onChangeConditions: function onChangeConditions(event) { var renderOptions = { refresh: {} }; this._reRender(this.appendExtraInfoToRenderOptions(renderOptions, event)); }, /** * Called when the container is entered. */ onEnterContainer: function onEnterContainer() { /* to be overridden */ }, /** * Called when the container is exited. */ onExitContainer: function onExitContainer() { /* to be overridden */ }, /** * update animation settings for transitions from one state to another according to the following rules: * 1) use the default defined in the definition properties unless override is set to exactly 0 (to turn it off) * 2) if no animation properties in the definition, use the override value if its > 0 * 3) if not in the def properties and not overridden by an action, animation should be off. * NOTE: by default overrideDefaultAnimationSpeed is -1 which denotes it is not enabled. */ updateAnimationSettings: function updateAnimationSettings(visdefProperties) { var visDefAnimationSpeed = visdefProperties && visdefProperties.animationSpeed ? visdefProperties.animationSpeed : 0; this.animationSpeed = visDefAnimationSpeed ? visDefAnimationSpeed : this.DEFAULT_ANIMATION_SPEED; if (this.overrideDefaultAnimationSpeed === 0 || this.visModel.getSuppressViewAnimations()) { //If an action has explicitly turned animation off, respect that option. this.animationSpeed = 0; } else if (this.overrideDefaultAnimationSpeed > 0 && visDefAnimationSpeed === 0) { //If animation is not on, use the override if its not off (-1) this.animationSpeed = this.overrideDefaultAnimationSpeed; } }, /** * VIPR supports animation effects (which are always enabled) * It is possible to do a fade-in/fade-out type animation for non-VIPR visualizations. * In this case, renderComplete should be called by animate not render. * NOTE: This style of animation has been DISABLED for Endor and is likely to be removed/reworked in future * as it simply makes non-VIPR visualizations flash needlessly (and appear slower). * @returns true if non-VIPR animation has been enabled for a particular visualization type */ isAnimationEnabledForNonVIPRVisualizations: function isAnimationEnabledForNonVIPRVisualizations() { return this.animationType && this.animationType !== this.ANIMATION_TYPES.NONE; }, /** * For now, for grid/summary/trend etc, there is only one animation effect by default which is to fade out and fade in */ animate: function animate(renderInfo) { var _this9 = this; this.visModel.renderCompleteBeforeAnimation(); if (this.isAnimationEnabledForNonVIPRVisualizations()) { var renderComplete = function renderComplete() { _this9.visModel.renderComplete(renderInfo); }; this.updateAnimationSettings(); $(this.el).fadeOut(0); //If doing fade-in/fade-out animation, we need to call render complete at the end. $(this.el).fadeIn(this.animationSpeed, renderComplete); } }, /** * Override this method to take the selection and highlight it. */ renderSelected: function renderSelected() {}, /** * Override this method to style focused item. */ renderFocused: function renderFocused() {}, /** * The base render method is called at the end of the subtype render() to provide any actions required * once render is prepared and submitted to the rendering engine (eg: RAVE). * * Its important to note that due to the async nature of rendering, this method may be called prior * to the actual render being completed. * @param {Object} renderInfo - renderInfo passed to all render methods (originates in the render sequence). * @param {boolean default=true} callRenderComplete - true if this function is being called at the end of the type-specific view render * so that renderComplete will be called. VIPRView is an exception. * VIPRView calls VisView:render to do common processing but needs to handle renderComplete itself because of the way it sets/decorates data. */ render: function render(renderInfo) { var callRenderComplete = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; this.animate(renderInfo); //Note: animate is overridden to do nothing for RAVE as it has effects support this.updateConditionalViewIndicator({ data: renderInfo.data }); if (callRenderComplete && !this.isAnimationEnabledForNonVIPRVisualizations()) { //If animation is enabled, we need to wait until the end of the animation to call render complete. //See (VisView:animate()). NOTE: This style of animation is disabled for Endor. this.visModel.renderComplete(renderInfo); } return Promise.resolve(this); }, /** * Removes the icon view element and remove hide class from the rave output area whose role is application */ removeIconView: function removeIconView() { if (!(this.hasUnavailableMetadataColumns() || this.hasMissingFilters())) { this._removeMissingDataColumnWarning(); } if (this.$iconView) { this.$el.find('div[role="application"]').removeClass('hide'); this.$el.parent().removeClass('showIconView'); this.$iconView.remove(); this.$iconView = null; } this._removeVisualizationDropZones(); }, _removeVisualizationDropZones: function _removeVisualizationDropZones() { var dropZonesOverlayState = this.content.getFeature('DropZonesOverlayState'); if (dropZonesOverlayState && dropZonesOverlayState.isEnabled()) { dropZonesOverlayState.hide(); } }, /** * Render warning container for this widget visualization */ _renderWarningContainer: function _renderWarningContainer($containerNode) { this._removeWarningContainer(); this.$warningContainer = $('
', { 'class': 'warningContainer' }); $containerNode.append(this.$warningContainer); }, /** * Remove warning container */ _removeWarningContainer: function _removeWarningContainer() { if (this.$warningContainer) { this.$warningContainer.remove(); this.$warningContainer = null; } }, _renderMissingDataColumnWarning: function _renderMissingDataColumnWarning($containerNode) { this._removeMissingDataColumnWarning(); if (this.hasMissingFilters() && this.filterIndicator && this.filterIndicator.$filter) { this.filterIndicator.$filter.addClass('dataWidgetFiltersNone'); } this.$dataUnavailable = $('
', { 'class': 'data-unavailable' }); this._renderWarningContainer($containerNode); this.$warningContainer.append(this.$dataUnavailable); var $warningIcon = $('
', { 'aria-label': StringResources.get('warning'), 'class': 'warningIcon' }); this.$dataUnavailable.append($warningIcon); Utils.setIcon($warningIcon, 'common-warning', StringResources.get('warning')); var $warningText = $('
', { 'class': 'warningText', 'text': StringResources.get('datasetItemsUnavailable') }); this.$dataUnavailable.append($warningText); }, _removeMissingDataColumnWarning: function _removeMissingDataColumnWarning() { if (this.$dataUnavailable) { this.$dataUnavailable.remove(); this.$dataUnavailable = null; } this._removeWarningContainer(); }, /** * Shows icon of the chart type and hide the rave output area whose role is application */ renderIconView: function renderIconView() { if (this._tabs) { this._tabs.hide(); } var $containerNode = this._prepareRenderIconView(); this._renderIconView($containerNode); this._renderVisualizationDropZones(); }, _renderVisualizationDropZones: function _renderVisualizationDropZones() { var dropZonesOverlayDOM = this.content.getFeature('DropZonesOverlayDOM'); var dropZonesOverlayState = this.content.getFeature('DropZonesOverlayState'); if (dropZonesOverlayState && dropZonesOverlayState.isEnabled() && !this.hasMissingFilters()) { if (dropZonesOverlayDOM.isMounted()) { dropZonesOverlayState.show(); } else { dropZonesOverlayDOM.render(); } } }, /** * Updates the smart annotations indicator if one exists */ updateSmartAnnotationsIndicator: function updateSmartAnnotationsIndicator() { var smartAnnotationsIndicator = this.content.getFeature('SmartsIndicator'); if (smartAnnotationsIndicator) { smartAnnotationsIndicator.update(); } }, updateConditionalViewIndicator: function updateConditionalViewIndicator(options) { var conditionalViewIndicator = this.content.getFeature('ConditionalViewIndicator'); if (conditionalViewIndicator) { conditionalViewIndicator.update(options); } }, /** * Updates the forecast indicator if one exists */ updateForecastIndicator: function updateForecastIndicator() { if (this.forecastIndicator) { this.forecastIndicator.update(); } }, /*Subclass can override the function when necessary*/ _renderIconView: function _renderIconView($containerNode) { $containerNode.append(this.$iconView); this.$el.find('div[role="application"]').addClass('hide'); this.$el.parent().addClass('showIconView'); this.visModel.renderCompleteBeforeAnimation(); this.visModel.renderComplete(); }, /** * Returns the content node. Can be overriden by sub classes. */ getContentNode: function getContentNode() { return this.contentNode ? $(this.contentNode) : this.$el; }, _prepareRenderIconView: function _prepareRenderIconView() { var definition = this.visualization.getDefinition(); var icon = definition.getPlaceholderIconUri(); var caption = definition.getLabel(); var $containerNode = this.getContentNode(); if (this.hasUnavailableMetadataColumns() || this.hasMissingFilters()) { this._renderMissingDataColumnWarning($containerNode); } else if (this.$dataUnavailable) { this._removeMissingDataColumnWarning(); } var template = dot.template(EmptyVisualizationTemplate); if (this.$iconView) { this.$iconView.remove(); } this.$iconView = $(template({ icon: icon, caption: caption, title: StringResources.get('LIVE_empty_visualization_hint_title'), description: StringResources.get('LIVE_empty_visualization_hint_description') })); return $containerNode; }, hasMissingFilters: function hasMissingFilters() { return this.ownerWidget.getUnavailableLocalFilter().length > 0; }, /** * Uses the widget size in a given render context */ resizeToWidget: function resizeToWidget(renderInfo) { this.resize(renderInfo.widgetSize); }, getLabel: function getLabel() { var label; var definition = this.visualization.getDefinition(); if (definition) { label = definition.getLabel(); } else { label = StringResources.get('visualizationLabel'); } return label; }, getDescription: function getDescription() { var label = this.getLabel(); var columns = this._getColumnInformationForLabel(); var description = StringResources.get('dataWidgetDescription', { widgetLabel: label, columnNames: columns }); return description; }, _getColumnInformationForLabel: function _getColumnInformationForLabel() { var columns = ''; var mappingInfo = this.visualization.getSlots().getMappingInfoList(); for (var i = 0; i < mappingInfo.length; i++) { if (i > 0) { columns += ', '; } columns += mappingInfo[i].dataItem.getLabel(); } return columns; }, getCurrentViewSelector: function getCurrentViewSelector() { return null; }, getProperties: function getProperties() { if (this.visModel.getDefinition().properties) { return Promise.resolve(this.visModel.getDefinition().properties.slice(0)); } return Promise.resolve([]); }, /** * @returns true if any any dataItems is binned */ hasBinnedDataItems: function hasBinnedDataItems() { return !!this.visualization.getSlots().getMappingInfoList().find(function (mappingInfo) { return !!mappingInfo.dataItem.getBinning(); }); }, /** * Generate thumbnail * * @return {Promise} */ generateThumbnail: function generateThumbnail() { var _this10 = this; var isDisabled = false; var isMappingIncomplete = false; if (this._isThumbnailDisabled()) { isDisabled = true; } if (!this.isMappingComplete()) { isMappingIncomplete = true; } var promise = void 0; if (isDisabled || isMappingIncomplete) { promise = Promise.resolve(); } else { promise = this.dashboardApi.getDashboardSvc('.Thumbnail').then(function (thumbnailSvc) { return thumbnailSvc.generateMarkup(_this10.$el.get(0)); }); } return promise.then(function (thumbnail) { return { isMappingIncomplete: isMappingIncomplete, isDisabled: isDisabled, thumbnail: thumbnail }; }); }, _isThumbnailDisabled: function _isThumbnailDisabled() { //TODO: Augment this to disable thumbnail for any declared unsupportedBrowsers // disable thumbnails only for IE11 and lower; Edge is supposedly behaving good in most cases if (BrowserUtils.isIE() && !BrowserUtils.isIEEdge()) { var defn = this.visModel.getDefinition(); if (defn && defn.thumbnailConfig) { //Disable thumbnails for visualizaions that delcares not supporting certain browsers var _defn$thumbnailConfig = defn.thumbnailConfig.unsupportedBrowsers, unsupportedBrowsers = _defn$thumbnailConfig === undefined ? [] : _defn$thumbnailConfig; return unsupportedBrowsers.indexOf('IE') !== -1; } } return false; }, onSummaryDataCellError: function onSummaryDataCellError() { var errorMsg = StringResources.get('errorCellWarning'); this.infoIndicator.addInfo([{ id: 'error_summary_cell', title: errorMsg, items: [{ id: 'error_summary_cell', label: errorMsg, isSubMessage: true }] }]); }, appendExtraInfoToRenderOptions: function appendExtraInfoToRenderOptions(renderOptions, event, extraInfo) { event = event && event.changeEvent || event; if (event && event.data) { if (renderOptions.extraInfo) { renderOptions.extraInfo.payloadData = event.data; } else { renderOptions.extraInfo = { payloadData: event.data }; } } if (renderOptions && renderOptions.extraInfo && extraInfo) { renderOptions.extraInfo = _.extend(renderOptions.extraInfo, extraInfo); } return renderOptions; } }); return VisView; }); //# sourceMappingURL=VisView.js.map