UNPKG

@domoinc/arrow-single-value-indicator

Version:

ArrowSingleValueIndicator - Domo Widget

680 lines (627 loc) 27.1 kB
//---------------------------------------------------------------------------------- //---------------------------------------------------------------------------------- // ArrowSingleValueIndicator: //---------------------------------------------------------------------------------- //---------------------------------------------------------------------------------- var d3 = require('d3'); var d3Chart = require('d3.chart'); var BaseWidget = require('@domoinc/base-widget'); var daTheme2 = require('@domoinc/da-theme2'); var SummaryNumber = require('@domoinc/summary-number'); module.exports = BaseWidget.extend('ArrowSingleValueIndicator', { //********************************************************************************** //********************************************************************************** initialize: function() { 'use strict'; var _Chart = this; //---------------------------------------------------------------------------------- // Config //---------------------------------------------------------------------------------- this._newConfig = { //plugin configs alignment: { name: 'Text Horizontal Alignment', description: 'The horizontal alignment of the text relative to the widget\'s chart bounds', value: {name: 'Align Text Left', value: 'left'}, type: 'select', options: [ {name: 'Align Text Left', value: 'left'}, {name: 'Align Text Right', value: 'right'}, {name: 'Center Text', value: 'center'} ] }, symbolAlignment: { name: 'Text Vertical Alignment', description: 'The vertical alignment of the prefix, suffix, and abbreviation symbol relative to the number value', type: 'select', value: {name: 'Align Text Bottom', value: 'bottom'}, options: [ {name: 'Align Text Top', value: 'top'}, {name: 'Align Text Bottom', value: 'bottom'}, ] }, prefixString: { name: 'Prefix', description: 'Text added before the indicator', type: 'string', value: '' }, postfixString: { name: 'Suffix', description: 'Text added after the indicator', type: 'string', value: '' }, textFontFamily: daTheme2.themeElements.h1FontFamily(), fontWeight: daTheme2.themeElements.h1FontWeight(), fontSize: { name: 'Font Size', description: 'Font size for the text', category: 'Text', value: 48, type: 'number', units: 'px' }, prefixFontSize: { name: 'Prefix Font Size', description: 'Font size for the prefix (if empty, the font size will be the same as the font size above)', category: 'Text', value: null, type: 'number', units: 'px' }, magnitudeFontSize: { name: 'Abbreviation Symbol Font Size', description: 'Font size for the abbreviation symbol for large numbers (e.g. if 1000 abbreviates to \'1K\', the symbol is \'K\')', category: 'Text', value: null, type: 'number', units: 'px' }, postfixFontSize: { name: 'Suffix Font Size', description: 'Font size for the suffix (if empty, the font size will be the same as the font size above)', category: 'Text', value: null, type: 'number', units: 'px' }, incTrendColor: daTheme2.themeElements.goodFill({ name: 'Increasing Trend Color' }), noTrendColor: daTheme2.themeElements.neutralFill({ name: 'No Trend Color' }), decTrendColor: daTheme2.themeElements.badFill({ name: 'Decreasing Trend Color' }), indicatorColor: daTheme2.themeElements.h1FontColor(), showArrow: { name: 'Show Trend Arrow', description: 'Show or hide the trend arrow', value: {name: 'Show', value: true}, type: 'select', options: [ {name: 'Show', value: true}, {name: 'Hide', value: false} ] }, hoverEvent: { name: 'Enable Mouse Animation', description: 'Enable or disable the animation triggered by moving the mouse over the widget', type: 'select', value: {name: 'Enable', value: true}, options: [ {name: 'Enable', value: true}, {name: 'Disable', value: false}, ] }, animationDuration: { name: 'Animation Duration', description: 'Duration of time for the initial animation', category: 'Animation', value: 500, type: 'number', units: 'ms' }, abbrNumber: { name: 'Number Format', description: 'Set the format of the number to be fully written out or abbreviated when the value is 1000 or greater (e.g. if abbreviated, 1000 will be 1K)', value: {name: 'Abbreviate', value: true}, type: 'select', options: [ {name: 'Abbreviate', value: true}, {name: 'Full Form', value: false}, ] }, numDecimal: { name: 'Decimal Places', description: 'Set the number of decimal places to be displayed', value: {name: 'Default', value: undefined}, type: 'select', options: [ {name: 'Default', value: undefined}, {name: 'None', value: 0}, {name: '.0', value: 1}, {name: '.00', value: 2}, {name: '.000', value: 3}, {name: '.0000', value: 4}, {name: '.00000', value: 5}, ] }, //widget default configs chartName: { description: 'Name of chart for Reporting.', type: 'string', value: 'ArrowSingleValueIndicator' }, height: { description: '', category: 'Dimensions', type: 'number', value: 0, units: 'px' }, width: { description: '', category: 'Dimensions', type: 'number', value: 0, units: 'px' }, offsetArrowLength: { description: 'Length of the arrow (0 is default, negative and positive is shorter and longer respectively)', category: 'Arrow', value: 0, type: 'number', units: 'px' }, offsetArrowWidth: { description: 'Width of the arrow (0 is default, negative and positive is narrower and wider respectively)', category: 'Arrow', value: 0, type: 'number', units: 'px' }, offsetArrowHeadWidth: { description: 'Width of the arrowhead (0 is default, negative and positive is narrower and wider respectively)', category: 'Arrow', value: 0, type: 'number', units: 'px' }, arrowOffset: { description: 'Distance of arrow from the text (negative is further away)', category: 'Arrow', value: -6, type: 'number' }, trend: { description: 'Flag which dictates whether or not to calculate a trend between two rows of data', value: false, type: 'boolean' } }; this.mergeConfig(_Chart._newConfig); //---------------------------------------------------------------------------------- // Data Definition: //---------------------------------------------------------------------------------- _Chart._newDataDefinition = { 'Label': { type: 'string', validate: function(d) { return true; }, accessor: function(line) { return String(line[0]); } }, 'Value': { type: 'number', validate: function(d) { return !isNaN(this.accessor(d)); }, accessor: function(line) { return Number(line[1]); }, default: 0 } }; this.mergeDataDefinition(_Chart._newDataDefinition); //---------------------------------------------------------------------------------- // Interface - chart's .on()' and document '.trigger()' //---------------------------------------------------------------------------------- //---------------------------------------------------------------------------------- // Static //---------------------------------------------------------------------------------- var service = new SummaryNumber(); var savedSummaryObject = service.summaryNumberToObject(0); //creates summary object. ie: Object {prefix: 0, magnitude: '', number: 0} savedSummaryObject.number = 0; var numberFormat = d3.format(',.0f'); //puts in commas and decimals var showArrow; _Chart._indicatorGroup = _Chart._layerGroup.append('g').attr('class', 'indicatorGroup'); _Chart._arrow = _Chart._indicatorGroup.append('g').attr('class', 'arrow'); _Chart._textPrefix = _Chart._indicatorGroup.append('text').attr({ 'class': 'prefix', 'id': 'prefix' }); _Chart._textValue = _Chart._indicatorGroup.append('text').attr({ 'class': 'value', 'id': 'value' }); _Chart._textMagnitude = _Chart._indicatorGroup.append('text').attr({ 'class': 'magnitude', 'id': 'magnitude' }); _Chart._textSuffix = _Chart._indicatorGroup.append('text').attr({ 'class': 'suffix', 'id': 'suffix' }); _Chart._hoverRect = _Chart._layerGroup.append('rect').attr('class', 'hoverRect').style('opacity', 0); _Chart._hoverRect .on('mouseover', hoverOn) .on('mouseout', hoverOff) //---------------------------------------------------------------------------------- // Data Functions //---------------------------------------------------------------------------------- //********************************************************************************** // Data validation function and chart variable setup. //********************************************************************************** this.transform = function(data) { var validData = _Chart.validateData(data); var number = _Chart.a('Value')(validData[0]); showArrow = _Chart.c('showArrow').value && _Chart.c('trend'); // only show arrow if the trend config is enabled updateDecimalFormat(number); calculateTrend(validData); //sets global flag to determine if data > 0, to determine indicator color setValueObject(number); //creates a number object, _Chart._valueObj = {prefix: 10, magnitude: 'K'}, where prefix is the number value, and magnitude is the symbol setText(savedSummaryObject.prefix, _Chart._valueObj.magnitude); //updates the text attrs for prefix string, suffix string, and value number. Also sets text value. updateTextBBoxValues(); //updates bbox values for each text element setArrowSize(_Chart._valueHeight, number); //set arrow size, and draws the arrow tweenNumber(number); //animates number, and updates text attrs and positions while animating return validData; }; //---------------------------------------------------------------------------------- // Helper Functions (called by transform) //---------------------------------------------------------------------------------- /** * update decimal format */ function updateDecimalFormat(number) { if (_Chart.c('numDecimal').value || _Chart.c('numDecimal').value === 0) { //if decimal format is set to none or .0, .00, etc. numberFormat = d3.format(',.0' + _Chart.c('numDecimal').value + 'f'); //set the number of decimal places to the number of decimals } else { //else if the decimal format is set to default if (_Chart.c('abbrNumber').value) { //if using number abbreviation numberFormat = d3.format(','); //use the default summaryNumber format for decimals } else { //if not using the summaryNumber format var numDec; if (String(number).indexOf('.') > -1) { //if there is a decimal in the number numDec = (number + '').split('.')[1].length; //find the length of the numbers after the decimal numberFormat = d3.format(',.' + numDec + 'f'); } else { numberFormat = d3.format(',.0' + 'f'); //else set the number of decimals in the format to 0 } } } } /** * determine if the value > 0 (require to determine font color later on) * @param {number} number - number value */ function calculateTrend(validData) { if (_Chart.c('trend')) { //if a trend can be made if (validData.length > 1) { if (_Chart.a('Value')(validData[0]) > _Chart.a('Value')(validData[1])) { //current value > last value _Chart._trendIsInc = 'true'; } else if (_Chart.a('Value')(validData[0]) < _Chart.a('Value')(validData[1])) { //current value < last value _Chart._trendIsInc = 'false'; } else { //current value === last value _Chart._trendIsInc = null; } } else { //only one data, trend is same _Chart._trendIsInc = null; } //is not trend widget } else { _Chart._trendIsInc = null; } } /** * Create number object: {prefix: number value; magnitude: magnitude symbol} * ie: Object: {prefix: 10, magnitude: 'K'} * @param {number} number - number value */ function setValueObject(number) { _Chart._valueObj = service.summaryNumberToObject(number, _Chart.c('numDecimal').value); } /** * updates the attributes for prefix and suffix strings, and value number. Also sets the text value. * @param {number} val - the abbreviated number value. so if value is 10000, the number = 10 * @param {string} magnitude - the magnitude symbol. so if the value is 10000, the string is 'K' */ function setText(val, magnitude) { updateTextAttrs(_Chart._textPrefix, checkIfFontSizeIsValid(_Chart.c('prefixFontSize')) , _Chart.c('prefixString')); updateTextAttrs(_Chart._textValue, _Chart.c('fontSize'), val); updateTextAttrs(_Chart._textSuffix, checkIfFontSizeIsValid(_Chart.c('postfixFontSize')), _Chart.c('postfixString')); } /** * update the bbox values of the text elements */ function updateTextBBoxValues () { _Chart._valueBBox = getElementWidthAndHeight(_Chart._textValue); _Chart._prefixBBox = getElementWidthAndHeight(_Chart._textPrefix); _Chart._magnitudeBBox = getElementWidthAndHeight(_Chart._textMagnitude); _Chart._suffixBBox = getElementWidthAndHeight(_Chart._textSuffix); _Chart._valueHeight = _Chart._valueBBox.height; _Chart._prefixWidth = _Chart._prefixBBox.width; } /** * Calculates and sets the arrows size * @param {number} Height - height of the arrow * @param {number} Number - number being drawn */ function setArrowSize(height, number) { _Chart._arrow.selectAll('*').remove(); if (!showArrow) { return; } else { if (_Chart._trendIsInc) { //if it's true or false _Chart._arrow.style('opacity', 1); } else { //if there is no trend, value is same _Chart._arrow.style('opacity', 0); } } //Calc 'Length' of the arrow. Based on height of the text. height = height * 0.35; height += d3.min([d3.max([_Chart.c('offsetArrowLength'), -height]), 500]); // add in offset var direction = _Chart._trendIsInc === 'true' ? 1 : -1; var halfHeight = height / 2.0; var arrowHeadLengthRatio = 15.0 / 25.0; var arrowLineWidthRatio = 6.0 / 25.0; //Calc arrow's line width var arrowWidth = Math.floor(arrowLineWidthRatio * height); //Add in offset arrowWidth += d3.min([ //Less than height d3.max([_Chart.c('offsetArrowWidth'), -arrowWidth]), //Greater than -arrowWidth height ]); // Calc arrow's head size var arrowHeadLength = arrowHeadLengthRatio * height; //Add in offset arrowHeadLength += d3.min([ d3.max([_Chart.c('offsetArrowHeadWidth'), -arrowHeadLength]), height ]); drawArrow(height, direction, halfHeight, arrowHeadLength, arrowWidth); _Chart._arrow.attr('transform', 'translate(' + (_Chart.c('arrowOffset') - _Chart._prefixWidth) + ',' + 0 + ')'); } /** * Reset text object to the current text (which will be the next 'old text' value). * Tweens text to the new value. * @param {number} Number - number being drawn */ function tweenNumber(number) { var tweenedNumber; //this is the number that will continualyl be changing _Chart._textValue .transition() .duration(_Chart.c('animationDuration')) .tween('text', function() { var i = d3.interpolate(savedSummaryObject.number, number); //savedSummaryObject.number is number it is transitioning from savedSummaryObject = service.summaryNumberToObject(number, _Chart.c('numDecimal').value); //saving savedSummaryObject as the number transitioning to, so when call widget again, transitions from that number savedSummaryObject.number = number; return function(t) { if (_Chart.c('abbrNumber').value) { tweenedNumber = service.summaryNumberToObject(parseFloat(i(t)), _Chart.c('numDecimal').value); this.textContent = numberFormat(tweenedNumber.prefix); updateTextAttrs(_Chart._textMagnitude, checkIfFontSizeIsValid(_Chart.c('magnitudeFontSize')), tweenedNumber.magnitude); } else { tweenedNumber = numberFormat(parseFloat(i(t))); this.textContent = tweenedNumber; updateTextAttrs(_Chart._textMagnitude, 0, ''); } updateTextPosition(); placeArrowAndText(); } }) .each('end', updateHoverRect); } //---------------------------------------------------------------------------------- // Helper Functions (called by other helper functions) //---------------------------------------------------------------------------------- /** * Sets styles, attributes, and text for the text element * @param {text element} textElement - current text element * @param {number} fontSize - font size * @param {string} textString - the string form of the text element */ function updateTextAttrs(textElement, fontSize, textString) { textElement .style({ 'font-family': _Chart.c('textFontFamily'), 'fill': setElementColor(), 'font-size': fontSize + 'px', 'font-weight': _Chart.c('fontWeight').value }) .attr('dy', _Chart.c('fontSize') * 0.35) .text(textString); } /** * Sets the color of the element depending on if the 'trend' config is set to true or not */ function setElementColor() { var elementColor; if (_Chart.c('trend')) { if (_Chart._trendIsInc === 'true') { elementColor = _Chart.c('incTrendColor'); } else if (_Chart._trendIsInc === 'false') { elementColor = _Chart.c('decTrendColor'); } else { elementColor = _Chart.c('noTrendColor'); } } else { elementColor = _Chart.c('indicatorColor'); } return elementColor; } /** * Check the given font size, if valid return it unchanged, * otherwise it returns the current fontSize. */ function checkIfFontSizeIsValid (currentFontSize) { return currentFontSize === null || currentFontSize === 0 ? _Chart.c('fontSize') : currentFontSize; } /** * get text elements' width and height * @param {text element} selection [current text element] */ function getElementWidthAndHeight(selection) { if (selection.text() !== '') { //if the text is not an empty string, get its bbox return selection.node().getBBox(); } else { //else return an object of 0,0 return {height: 0, width: 0}; } } /** * Draws the Indicator Arrow * @param {number} height - Over all height of the arrow to be drawn * @param {number} direction - [1,-1]->aka:[up,down] * @param {number} halfHeight - height / 2.0 * @param {number} arrowHeadLength - Length of the arrow * @param {number} arrowWidth - Width of the arrow line */ function drawArrow(height, direction, halfHeight, arrowHeadLength, arrowWidth) { _Chart._arrow.append('line') .attr({ x1: -height, y1: (direction * halfHeight), x2: -arrowHeadLength / 2.0, y2: ((-halfHeight + (arrowHeadLength / 2.0)) * direction) }) .style({ fill: 'none', stroke: setElementColor(), 'stroke-width': arrowWidth }); _Chart._arrow.append('polygon') .attr('points', (-arrowHeadLength + ',' + (-halfHeight * direction)) + ' ' + (0 + ',' + (-halfHeight * direction)) + ' ' + (0 + ',' + ((-halfHeight * direction) + (arrowHeadLength * direction))) ) .style('fill', setElementColor()); } /** * translates all tspans to correct positions */ function updateTextPosition() { updateTextBBoxValues(); var valueTranslateX = 0; //value translate is our frame of reference. everything translated around this text element. var prefixTranslateX = -_Chart._prefixBBox.width; var maginitudeTranslateX = _Chart._valueBBox.width; var suffixTranslateX = _Chart._valueBBox.width + _Chart._magnitudeBBox.width; checkText(_Chart._textPrefix, prefixTranslateX, true); checkText(_Chart._textValue, valueTranslateX, false); checkText(_Chart._textMagnitude, maginitudeTranslateX, true); checkText(_Chart._textSuffix, suffixTranslateX, true); } /** * check if the text is an empty or not * if empty, translate it to 0,0 to the alignment isn't thrown off by an empty text */ function checkText(textElement, translateValueX, translateY) { if (!textElement.text) { return; } if (textElement.text() === '') { textElement.attr('transform', 'translate(0,0)'); return; } var translateValueY = 0; if (translateY && _Chart.c('symbolAlignment').value === 'top') { var symbolHeight = textElement.node().getBBox().height; var valueYPos = _Chart._valueBBox.y; translateValueY = symbolHeight ? valueYPos + symbolHeight/2 : 0; } textElement.attr('transform', 'translate(' + translateValueX + ',' + translateValueY + ')'); } /** * Places the arrow and text [center, left, right] aligned. */ function placeArrowAndText() { _Chart._arrowBBox = _Chart._arrow.node().getBBox(); _Chart._indicatorBBox = _Chart._indicatorGroup.node().getBBox(); if (!showArrow) { switch (_Chart.c('alignment').value) { case 'left': _Chart._indicatorGroup.attr('transform', 'translate(' + _Chart._prefixBBox.width + ',0)'); break; case 'right': _Chart._indicatorGroup.attr('transform', 'translate(' + (-_Chart._valueBBox.width - _Chart._magnitudeBBox.width - _Chart._suffixBBox.width) + ',0)'); break; case 'center': _Chart._indicatorGroup.attr('transform', 'translate(' + (-_Chart._indicatorBBox.width/2 + _Chart._prefixBBox.width) + ',0)'); break; default: _Chart._indicatorGroup.attr('transform', 'translate(' + _Chart._prefixBBox.width + ',0)'); console.warn('Invalid aligmnet set. Can\'t make alignment adjustments.'); } } else { switch (_Chart.c('alignment').value) { case 'left': if (!_Chart._trendIsInc) { _Chart._indicatorGroup.attr('transform', 'translate(' + (_Chart._prefixBBox.width) + ',' + 0 + ')'); } else { _Chart._indicatorGroup.attr('transform', 'translate(' + (_Chart._arrowBBox.width + _Chart._prefixBBox.width - _Chart.c('arrowOffset')) + ',' + 0 + ')'); } break; case 'right': _Chart._indicatorGroup.attr('transform', 'translate(' + (-_Chart._valueBBox.width - _Chart._magnitudeBBox.width - _Chart._suffixBBox.width) + ',' + 0 + ')'); break; case 'center': var totalWidth = _Chart._indicatorBBox.width; var arrowAndPrefixWidth = _Chart._arrowBBox.width + _Chart._prefixBBox.width - _Chart.c('arrowOffset'); var arrowAndOffset = _Chart._arrowBBox.width - _Chart.c('arrowOffset'); if (!_Chart._trendIsInc) { _Chart._indicatorGroup.attr('transform', 'translate(' + (_Chart._prefixBBox.width - (totalWidth - arrowAndOffset)/2) + ',' + 0 + ')'); } else { _Chart._indicatorGroup.attr('transform', 'translate(' + (-(totalWidth/2 - arrowAndPrefixWidth)) + ',' + 0 + ')'); } break; default: console.warn('Invalid aligmnet set. Can\'t make alignment adjustments.'); _Chart._indicatorGroup.attr('transform', 'translate(' + 0 + ',' + 0 + ')'); } } } /** * update dimensions of the hover rect */ function updateHoverRect() { var indicatorGroup = _Chart._indicatorGroup.node().getBBox(); var indicatorPos = d3.transform(d3.select(this.parentNode).attr('transform')).translate; //jshint ignore:line var arrowBBox = _Chart._layerGroup.select('.arrow').node().getBBox(); var prefixBBox = _Chart._layerGroup.select('[id^=prefix]').node().getBBox(); _Chart._hoverRect.attr({ transform: function() { if (showArrow) { return 'translate(' + (indicatorPos[0] + arrowBBox.x + _Chart.c('arrowOffset') - prefixBBox.width) + ',' + indicatorGroup.y + ')'; } else { return 'translate(' + (indicatorPos[0] - prefixBBox.width) + ',' + indicatorGroup.y + ')'; } }, width: function() { if (showArrow) { return indicatorGroup.width; } else { return indicatorGroup.width - arrowBBox.width + _Chart.c('arrowOffset'); } }, height: indicatorGroup.height, }); } //---------------------------------------------------------------------------------- // Event Functions //---------------------------------------------------------------------------------- /** * increase indicator text size when hover */ function hoverOn() { if (_Chart.c('hoverEvent').value) { _Chart._layerGroup .transition() .ease('elastic') .attr('transform', 'scale(1.05)'); } } /** * restore text size when hover off */ function hoverOff() { if (_Chart.c('hoverEvent').value) { _Chart._layerGroup .transition() .ease('elastic') .attr('transform', 'scale(1)'); } } } });