'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