'use strict'; /** * Licensed Materials - Property of IBM * IBM Cognos Products: BI Cloud (C) Copyright IBM Corp. 2019, 2020 * US Government Users Restricted Rights - Use, duplication or disclosure restricted by GSA ADP Schedule Contract with IBM Corp. */ define(['jquery', 'underscore', '../VisView', 'text!./Infographic.html', '../../../util/DashboardFormatter', '../../../widgets/livewidget/nls/StringResources', '../../../lib/@waca/dashboard-common/dist/utils/ScaleUtil', './InfographicScaleView', '../../../lib/@waca/dashboard-common/dist/ui/AuthoringToolbar', '../../../lib/@waca/core-client/js/core-client/utils/Utils', '../../../lib/@waca/core-client/js/core-client/utils/BrowserUtils', '../../../lib/@waca/dashboard-common/dist/utils/EventChainLocal', '../VisEventHandler', './InfographicEventTarget', '../../../DynamicFileLoader'], function ($, _, VisView, InfographicTemplate, Formatter, stringResources, ScaleUtil, InfographicScaleView, Toolbar, Utils, BrowserUtils, EventChainLocal, VisEventHandler, InfographicEventTarget, DynamicFileLoader) { 'use strict'; // Offset used with size of shapes calculations var OFFSET = 1; /** * The InfographicView renders one or more aggregated facts * eg: Total Revenue for the currently selected filter values. */ var InfographicView = VisView.extend({ className: 'dataview infographic-widget', templateString: InfographicTemplate, events: { 'primaryaction .infographic-scale-wrapper': 'infographicToolbar' }, /** * Initialize the view and its handlers, then render. */ init: function init() { InfographicView.inherited('init', this, arguments); this.animationType = this.ANIMATION_TYPES.NONE; this.toolbarShown = false; this.isPercent = false; this.isFirstRender = true; this.visModel.on('change:theme', this.onChangeTheme, this); this._colorsService = this.dashboardApi.getFeature('Colors'); this._translationService = this.dashboardApi.getDashboardCoreSvc('TranslationService'); this._showTranslationIcon = false; }, // @override remove: function remove() { if (this.eventHandler) { this.eventHandler.remove(); this.eventHandler = null; } if (this.visModel) { this.visModel.off('change:theme', this.onChangeTheme, this); } this.visControl = null; InfographicView.inherited('remove', this, arguments); }, getRenderer: function getRenderer() { return 'gemini/dashboard/visualizations/renderer/infographic/InfographicRenderer'; }, /** * Infographic view overwrites the default animation in order to avoid it fading in/out when * applying filters. */ //@Override animate: function animate() { // Rendering is complete, no animation yet this.visModel.renderCompleteBeforeAnimation(); }, /** * Loads and creates the control. Returns a promise which is resolved when the control * is created and ready to render */ //TODO: Remove function whenVisControlReady: function whenVisControlReady() { var _this = this; if (this.visControl) { return Promise.resolve(this.visControl); } else { return DynamicFileLoader.load(['dashboard-analytics/visualizations/renderer/infographic/control/InfographicControl']).then(function (modules) { var VisControl = modules[0]; _this.visControl = new VisControl({ domNode: _this.$el.find('.infographic-widget-content')[0] }); //TODO: Retain event target _this.eventHandler = new VisEventHandler({ target: new InfographicEventTarget({ $el: _this.$el, visControl: _this.visControl, visAPI: _this.visModel, view: _this }), transaction: _this.transactionApi, ownerWidget: _this.ownerWidget, visAPI: _this.visModel, edgeSelection: true }); return _this.visControl; }); } }, _readyToRender: function _readyToRender() { //to cover the case where the slot is changed to an infographic and need a shape return this.isMappingComplete() && !this.hasMissingFilters() && !this.hasUnavailableMetadataColumns(); }, /** * render the results. * If results are not available, fetch them first. * @param {Object} renderInfo - renderInfo passed to all render methods from the render sequence. * @returns a promise which is resolved when the render is complete. */ render: function render(renderInfo) { this.graphicFillColor = this.content.getPropertyValue('value.graphic.fillColor'); this.graphicCurrentScaleOption = this.content.getPropertyValue('value.graphic.currentScaleOption'); this.graphicContent = this.content.getPropertyValue('value.graphic.content'); var doFillAnimation = !BrowserUtils.isIE(); if (!this._readyToRender()) { this.resizeToWidget(renderInfo); this.renderIconView(); return Promise.resolve(this); } this.removeIconView(); this.resizeToWidget(renderInfo); var html = this.buildHTML(renderInfo.data.getResult(), doFillAnimation); this.addToDom(html); this.applyVisModelProperties(); Utils.embedSVGIcon(this.$el); // patch disappearing SVGs in win10 IE11 this.updateTranslationIcon(); return InfographicView.inherited('render', this, arguments); }, /** * Shows icon of the chart type and hide the rave output area whose role is application * @override VisView._renderIconView */ _renderIconView: function _renderIconView() { if (this.$el) { this.$el.closest('.infographic-widget').empty().append(this.$iconView); } this.visModel.renderCompleteBeforeAnimation(); this.visModel.renderComplete(); }, summaryReveal: function summaryReveal() { this.visModel.renderCompleteBeforeAnimation(); if (!this._readyToRender()) { return Promise.resolve(); } var animateClass = this._fillVertically() ? 'fillVertical' : 'fillHorizontal'; var reflowShape = function reflowShape(el) { el.classList.remove(animateClass); void el.clientWidth; // forces repaint which triggers fill animation el.classList.add(animateClass); }; this.$el.find('.animate').each(function (i, el) { reflowShape(el); }); return Promise.resolve(); }, /** * @override VisView.removeIconView * */ removeIconView: function removeIconView() { if (this.$iconView && this.$el) { this.$el.find('div[class="value"]').addClass('hide'); this.$el.find('div[class="label"]').addClass('hide'); this.$iconView.remove(); this.$iconView = null; } }, getDescription: function getDescription() { var description; if (this.infographicInfo) { var label = this.getLabel(); var infographic = stringResources.get('infographicLabel', { label: this.infographicInfo.label, value: this.infographicInfo.v }); description = stringResources.get('WidgetLabelWithDescripion', { label: label, description: infographic }); } else { description = InfographicView.inherited('getDescription', this, arguments); } return description; }, /** * Creates and displays toolbar for updating/changing scale options, with inner content * @param event - event that caused this function to be triggered. Contains target, which is used to determine where to place toolbar * @returns promise resolved when the toolbar is rendered and displayed to user */ infographicToolbar: function infographicToolbar(event) { // Prevent widget ODT from displaying/updating new EventChainLocal(event).setProperty('preventDefaultContextBar', true); if (this.graphicContent && !this.toolbarShown) { var toolbar = new Toolbar({ container: $('body'), placement: 'auto bottom', calculateBoundingRect: true }); var infoScale = new InfographicScaleView({ content: this.content, visModel: this.visModel, widget: this.visModel.ownerWidget, widgetValue: this.infographicValue, isPercent: this.isPercent, currentScaleOption: this.graphicCurrentScaleOption }); toolbar.addItems([{ responsive: false, editable: false, changedAction: null, subView: infoScale, type: 'SubView' }]); this.visModel.ownerWidget.on('expandedView:visible', this.onShowExpandedContent.bind(this, toolbar)); this.$el.closest('.page.pageabsolute, .page.pagecontainer').on('scroll.BaseController', this.scaleToolbarOnScroll.bind(this, toolbar)); this.toolbarShown = true; // Checks when the update scale toolbar is closed toolbar.on('toolbar:hide', this.onToolbarClosed, this); toolbar.on('toolbar:show', function () { infoScale.setFocus(); }); toolbar.setSelectionContext([event.target]); return toolbar.show(); } else { return undefined; } }, /** * Event handler called when toolbar is closed * @returns Sets that no toolbar exists */ onToolbarClosed: function onToolbarClosed() { this.toolbarShown = false; }, /** * Closes the scale toolbar when expanded view is shown * @param toolbar - toolbar object that needs to be closed * @param event - Event that triggered the function call * @returns {undefined} toolbar is closed and event listener turned off */ onShowExpandedContent: function onShowExpandedContent(toolbar) { this.visModel.ownerWidget.off('expandedView:visible', this.onShowExpandedContent, this); toolbar.hide(); }, /** * Closes the scale toolbar on page scroll * @param toolbar - toolbar object that needs to be closed * @param event - Event that triggered the function call (need to confirm it is a scroll we want to close the toolbar on) * @returns {undefined} If valid page scroll the toolbar is closed */ scaleToolbarOnScroll: function scaleToolbarOnScroll(toolbar, event) { if (event.target.classList.contains('page') && (event.target.classList.contains('pageabsolute') || event.target.classList.contains('pagecontainer'))) { //remove event listener when the toolbar is hidden this.$el.closest('.page.pageabsolute, .page.pagecontainer').off('scroll.BaseController'); toolbar.hide(); } }, /** * Create the HTML output * @param queryResults The query results * @param doFillAnimation Whether or not the fill animation should be rendered */ buildHTML: function buildHTML(queryResults, doFillAnimation) { this.infographicInfo = {}; var slots = this.content.getFeature('Visualization').getSlots().getMappedSlotList(); if (!slots || slots.length === 0) { return Promise.resolve(''); } this.infographicValue = queryResults && queryResults.getRowCount() > 0 ? queryResults.getValue(0, 0).value : null; this.infographicInfo = this._buildHtml(slots[0], this.infographicValue, doFillAnimation); return this.dotTemplate(this.infographicInfo); }, /** * Listener to rerender when theme changes (default palette changes with * theme) */ onChangeTheme: function onChangeTheme() { this.visModel.getRenderSequence().reRender(); }, _showItemLabel: function _showItemLabel() { return this.visModel.getPropertyValue('showItemLabel'); }, _getCustomLabel: function _getCustomLabel() { return this.visModel.getPropertyValue('baseValueLabel'); }, _fillVertically: function _fillVertically() { return this.visModel.getPropertyValue('fillDirection') === 'BottomToTop'; }, /** * Return text formatted value for field to avoid jsHint cyclomatic * complexity warnings * * @param slot, slotAPI * @param v The raw fact value. * @returns text formatted data value */ _valueFormat: function _valueFormat(dataItem, v) { var formatSpec = dataItem.getFormat(); v = Formatter.format(v, formatSpec); if (v.length < 5) { // if the formatted value is less than 5 characters long, it // looks ugly when stretched so we are adding spaces before and // after. The SVG will size it nicely after that. var sPrefix = v.length < 3 ? ' ' : ' '; v = sPrefix + v + sPrefix; } return v; }, _getForegroundColor: function _getForegroundColor() { var hexColor = this._colorsService.getHexColorFromDashboardColorSet(this.visModel.getPropertyValue('elementColor')); if (hexColor === 'transparent') { hexColor = ''; } return hexColor; }, getScaleOption: function getScaleOption() { var result; if (typeof this.graphicCurrentScaleOption !== 'undefined') { result = this.graphicCurrentScaleOption; } else { result = ScaleUtil.SCALE_VALUE_DEFAULT; } return result; }, getScaleValue: function getScaleValue(scaleProperties, currentOption, value) { var result; if (scaleProperties && typeof currentOption !== 'undefined') { var scales = scaleProperties.availableScales; if (currentOption === ScaleUtil.SCALE_VALUE_DEFAULT) { // Default value result = scaleProperties.optimalScale; } else if (currentOption === ScaleUtil.SCALE_VALUE_MANY) { // Many var scaleManyIndex = scales.indexOf(scaleProperties.optimalScale) - 1; if (scaleManyIndex < 0) { scaleManyIndex = 0; } result = scales[scaleManyIndex]; } else { // Few var scaleFewIndex = scales.indexOf(scaleProperties.optimalScale) + 1; if (scaleFewIndex > scales.length - 1) { scaleFewIndex = scales.length - 1; } result = scales[scaleFewIndex]; } } else if (value) { result = ScaleUtil.calcOptimalValue(value); } return result; }, // livewidget_cleanup there might be a defect here... // the correnspoding unittest: `render as infographic` is not passing.. _calcScale: function _calcScale(v) { var scaleObj = {}; var scaleProperties = ScaleUtil.getScalingProperties(v, this.isPercent); scaleObj.currentScaleOption = this.getScaleOption(); scaleObj.scale = this.getScaleValue(scaleProperties, scaleObj.currentScaleOption, v); scaleObj.abbreviatedScale = Formatter.format(scaleObj.scale, { decimalFormatLength: 'short' }); // Determine the number of shapes and partial shapes to display scaleObj.scaleComponents = ScaleUtil.getScaleComponents(v, scaleObj.scale, this.isPercent); return scaleObj; }, _isPercent: function _isPercent(dataItem, v) { var dataItemFormat = dataItem.getFormat(); return dataItemFormat && dataItemFormat.type === 'percent' && Math.abs(v) <= 1; }, _constructShapeStyle: function _constructShapeStyle(hexColor) { var shapeStyle = {}; var property = 'fill:'; var colorClass = 'colorFill'; shapeStyle.svgClass = 'Shape'; if (this.graphicFillColor === 'transparent') { colorClass = 'colorStroke'; property = 'stroke:'; shapeStyle.svgClass = 'Line'; } if (hexColor !== '') { shapeStyle.colorClass = ''; shapeStyle.coloredStyle = 'style="' + property + hexColor + '"'; } else { shapeStyle.colorClass = colorClass; shapeStyle.coloredStyle = ''; } return shapeStyle; }, /** * Return information needed to construct the HTML output The values and * html fragments returned construct the output. * * @param slot The slot item which gets its label rendered when showItemLabel is true. * @param v The fact value which is rendered into a scaleable svg viewBox. * @param doFillAnimation Whether or not the fill animation should be rendered * @returns a data structure with values to substitute into the html templateString. */ _buildHtml: function _buildHtml(slot, v, doFillAnimation) { var dataItemIndex = 0; //Infographic view only has one data item var dataItem = slot.getDataItemList()[dataItemIndex]; var hexColor = this._getForegroundColor(); var infographic; var fillVertical = this._fillVertically(); var shapeStyle = this._constructShapeStyle(hexColor); this.isPercent = this._isPercent(dataItem, v); var scaleObj = this._calcScale(v); var scale = scaleObj.scale; var scaleComponents = scaleObj.scaleComponents; // keep original viewbox var matches = this.graphicContent.match(RegExp('viewBox="(.*?)"')); var viewBox = matches && matches[1] ? matches[1] : '0 0 100 100'; var svgContent = this._extractSvgContent(this.graphicContent); var clipPathLastElement = this._calculateClipPath({ x: viewBox.split(' ')[0], y: viewBox.split(' ')[1], width: viewBox.split(' ')[2], height: viewBox.split(' ')[3] }, scaleComponents, fillVertical, doFillAnimation); infographic = { content: svgContent, scaleComponents: scaleComponents, percentageScaleValue: this._getPercentageScaleValue(v, scale), scale: scale, abbreviatedScale: scaleObj.abbreviatedScale, viewBox: viewBox, svgId: _.uniqueId('svgDef'), style: shapeStyle, fillClass: fillVertical ? 'fillVertical' : 'fillHorizontal', doFillAnimation: doFillAnimation, // 0.5 -> 500ms animation fillDuration: 0.5 / (scaleComponents.numShapes + 1), clipPathLastElement: clipPathLastElement }; // Update the graphic model data with things calculated this.currentScaleOption = scaleObj.currentScaleOption; this.numOfShapes = scaleComponents.numShapes + scaleComponents.numGreyedShapes; if (scaleComponents.partialValue > 0) { this.numOfShapes++; } v = this._valueFormat(dataItem, v); return { v: v, showTitleString: this._showItemLabel(), label: this._getCustomLabel() || dataItem.getLabel(), infographic: infographic }; }, /** * Calculates the string to display for the percentage scale value (i.e. +/- 1%, 10%, or 100%) * @param value - The percentage value we are displaying * @param scale - The current scale value being used * @returns String to be displayed for legend */ _getPercentageScaleValue: function _getPercentageScaleValue(value, scale) { if (!this.isPercent) { return false; } var result = scale * 100; if (value < 0) { result *= -1; } return Formatter.format(result / 100, { type: 'percent', maximumFractionDigits: 1, minimumFractionalDigits: 1 }); }, /** * A helper function that calculates clip path * @param {object} viewBoxProp - properties of viewBox {x:xxx, y: xxx, width: xxx, height: xxx} * @param {object} scale - scale componenets * @param {Boolean} isVertical - true if the shapes are filled from bottom to top * @return {object} - a clipPath {x: xxx, y: xxx, width: xxx, height: xxx} */ _calculateClipPath: function _calculateClipPath(viewBoxProp, scale, isVertical) { // calculate clipPath width and height based on the viewBox var greyedClipPath; var partialValue; if (isVertical) { //viewBoxProp members variable are strings. Convert viewBoxProp.x to do the arithmetic operation partialValue = viewBoxProp.height * (1 - scale.partialValue) + Number(viewBoxProp.y); greyedClipPath = { width: '100%', y: partialValue }; } else { //viewBoxProp members variable are strings. Convert viewBoxProp.x to do the arithmetic operation partialValue = viewBoxProp.width * scale.partialValue + Number(viewBoxProp.x); greyedClipPath = { width: partialValue, y: 0 }; } return greyedClipPath; }, /** * a helper method extract the content inside of a SVG Element * @param {string} content - a string that contains svg tag. eg. * @return {string} svgContent - a string that does not contain svg tag. eg. */ _extractSvgContent: function _extractSvgContent(content) { var $childrenContent = $(content).children(); // eslint-disable-next-line no-undef var serializer = new XMLSerializer(); var svgContent = ''; $childrenContent.each(function () { svgContent += serializer.serializeToString(this); }); return svgContent; }, /** * Take some HTML and render it to the dom element for this view. * * @param sHtml A string of HTML to render. */ addToDom: function addToDom(sHtml) { this.$el.empty(); this.$el.prepend(sHtml); this.setElement(this.$el); }, onVisible: function onVisible() { if (this.renderOnVisible) { this._resize(); } }, /** * Apply widget-level properties as specified either in overrides in the * visModel or defaults in the definition. */ applyVisModelProperties: function applyVisModelProperties() { this._resizeInfographicShapes(); }, _resizeInfographicShapes: function _resizeInfographicShapes() { // return if nothing to show if (this.numOfShapes === 0) { return; } var $node = $(this.el).find('.infographic'); var width = Math.floor($node.width()); //set the height to 1 if it is zero to avoid dividing by 0 case var height = Math.floor($node.height()) || 1; var x = width, y = height, n = this.numOfShapes; var px = Math.ceil(Math.sqrt(n * x / y)); var sx, sy; if (Math.floor(px * y / x) * px < n) { sx = y / Math.ceil(px * y / x); } else { sx = x / px; } var py = Math.ceil(Math.sqrt(n * y / x)); if (Math.floor(py * x / y) * py < n) { sy = x / Math.ceil(x * py / y); } else { sy = y / py; } // UX design of 8 pixels of padding (4 on each side) around each var shapePadding = 8; // Subtract an extra pixel for small spacing issue var size = Math.max(sx, sy) - shapePadding - OFFSET; var sizePx = size + 'px'; this.$el.find('.infographic .infographic-content').each(function () { // Going through jQuery here was really expensive on Safari, so go straight to the style attributes. this.style.width = this.style.height = sizePx; }); }, preRenderFillDirectionProperty: function preRenderFillDirectionProperty() { return { 'removeProperty': false }; }, setShowTranslationIcon: function setShowTranslationIcon(showTranslationIcon) { this._showTranslationIcon = showTranslationIcon; }, updateTranslationIcon: function updateTranslationIcon() { var $iconContainer = this.$el.closest('.widgetContentWrapper'); if (this._showTranslationIcon) { this._translationService.appendTranslationIcon($iconContainer); } else { this._translationService.removeTranslationIcon($iconContainer); } }, getDecoratorAPI: function getDecoratorAPI() { var _this2 = this; return { decorateItem: function decorateItem() { _this2.setShowTranslationIcon(true); }, decoratePoint: function decoratePoint() { return null; }, decorate: function decorate() { return null; }, clearItemDecoration: function clearItemDecoration() { _this2.setShowTranslationIcon(false); }, updateDecorations: function updateDecorations() { _this2.updateTranslationIcon(); } }; } }); return InfographicView; }); //# sourceMappingURL=InfographicView.js.map