UNPKG

@domoinc/multiline-chart

Version:

MultiLineChart - Domo Widget

854 lines (733 loc) 28.2 kB
var d3 = require('d3'); var d3Chart = require('d3.chart'); var _ = require('lodash'); var da = require('@domoinc/utilities'); var BaseWidget = require('@domoinc/base-widget'); var datheme = require('@domoinc/da-theme2'); //---------------------------------------------------------------------------------- //---------------------------------------------------------------------------------- // DomoTooltip: //---------------------------------------------------------------------------------- //---------------------------------------------------------------------------------- module.exports = BaseWidget.extend("DomoTooltip", { initialize: function () { var _Chart = this; //---------------------------------------------------------------------------------- // Default Values for getter/setters: // Set the default values of your Getter/Setter functions. //---------------------------------------------------------------------------------- this._newConfig = { chartName: { description: 'Name of chart for Reporting.', type: 'string', value: 'Dropdown' }, container: { description: 'Container for the tooltip to get its bounds from', type: 'd3 selected element', value: this.base }, opacity: { description: 'Opacity of the tooltip. Useful for testing', type: 'number', value: 0 }, padding: { description: 'Padding for the group that goes into the tooltip', type: 'number', value: 7 }, caretSize: { description: 'Size of the caret on the tooltip', type: 'number', value: 5 }, stationary: { description: 'True if you want the tooltip stationary', type: 'boolean', value: false }, maxWidth: { description: 'Max width for the tooltip', type: 'number', value: 200 }, format: { description: 'Format for the group in the tooltip', type: 'string', value: 'default' }, tooltipBackgroundColor: datheme.themeElements.tooltipBackground(), autoTriggerMoveToEvent: { description: 'If true, attached moveTo event listener to chart.base.', type: 'boolean', value: false } }; this.mergeConfig(_Chart._newConfig); //---------------------------------------------------------------------------------- // FORMATS //---------------------------------------------------------------------------------- var formats = { /** * Default */ 'default': function (appends) {}, /** * Text */ 'text': function (appends) { //Automatically position if there is only one text element appended if (appends[0].length === 1 && appends.node().childNodes[0].nodeName === 'text') { var textBBox = appends.node().childNodes[0].getBBox(); appends.select('text') .attr({ dy: function () { return getCentralPosition(this); }, y: textBBox.height / 2 }); appends.selectAll('tspan') .attr({ dy: function () { return getCentralPosition(this); }, y: textBBox.height / 2 }); } }, /** * Text Block */ 'textBlock': function (appends) { var textElement = appends.select('text'); var dy = textElement.node().getBoundingClientRect().height; d3.domoStrings.wrapText(textElement, _Chart.c('maxWidth')); appends.select('text').attr({y: dy - (dy / 4)}); }, /** * Text Rect Text */ 'textRectText': function (appends) { //Automatically format for text rect text pattern if (appends.node().childNodes.length % 2 !== 0) { if (isTextRectText(appends)) { formatAppendTextRectText(appends); } } } }; //---------------------------------------------------------------------------------- // Static - Anything that is defined here will never change //---------------------------------------------------------------------------------- var r = 3; //Radius to round the rect this._baseGroup = this.base.append("g") .attr('transform', 'translate(0, 0)') .style('pointer-events', 'none'); _Chart._parent = _Chart._baseGroup.node().parentNode; this._baseGroup.append('path') .attr('class', 'toolTipPath') .style('opacity', 0.9); this._appends = this._baseGroup.append('g'); _Chart._drawn = false; _Chart._moveX = undefined; _Chart._moveY = undefined; //---------------------------------------------------------------------------------- // Data validation function and chart variable setup. //---------------------------------------------------------------------------------- this.transform = function (data) { _Chart._container = _Chart.c('container'); setupMoveToListener(); return data; }; /** * This will hide the tooltip properly */ this.on('remove', function () { _Chart._drawn = false; if (d3.select('html').node().contains(_Chart._baseGroup.node())) { _Chart._parent.removeChild(_Chart._baseGroup.node()); } }); /** * This will re-draw the tool tip with the correct dimensions around the _appends group * @param {number} width override the width of the container * @param {number} height override the height of the container */ this.on('draw', function (width, height) { _Chart._parent.appendChild(_Chart._baseGroup.node()); _Chart._container = _Chart.c('container'); _Chart._caretWidth = _Chart.c('caretSize') * 2; _Chart._containerBBox = _Chart.c('container').node().getBoundingClientRect(); //Format _Chart._baseGroup.select('.toolTipPath').style('fill', _Chart.c('tooltipBackgroundColor')); _Chart._appends.attr('transform', 'translate(' + _Chart.c('padding') + ', ' + _Chart.c('padding') + ')'); formats[_Chart.c('format')](_Chart._appends); setGBBox(height, width); setBaseGroupBBox(); generatePaths(); _Chart._baseGroup .select('.toolTipPath') .attr({ 'transform': _Chart._topPath.translate, d: _Chart._topPath.path }); _Chart._drawn = true; }); /** * This will move the tooltip wherever you will it to be * @param {object} point Object with an x and y * @param {string} location String that will overide the location of the tooltip * @param {number} newWidth This will override the width of the tooltip's bounds * @param {number} newHeight This will override the height of the tooltip's bounds */ this.on('moveTo', function (point, location, newWidth, newHeight) { setBaseGroupBBoxDimensions(location); if (_Chart._drawn) { var containerWidth = newWidth ? newWidth : _Chart._containerBBox.width; var containerHeight = newHeight ? newHeight : _Chart._containerBBox.height; var baseGroup = _Chart._baseGroup; var tooltipPositionObj = getTooltipPositionStatus(containerWidth, containerHeight, point); if (!location) { location = determineInboundsTooltipLocation(tooltipPositionObj); } offsetCaretAtEdge({ left: tooltipPositionObj.hittingLeft, right: tooltipPositionObj.hittingRight, top: tooltipPositionObj.hittingTop, bottom: tooltipPositionObj.hittingBottom, container: { height: containerHeight, width: containerWidth }, point: point }); var posAndPathObj = calPositionAndPath(location, tooltipPositionObj, containerHeight, containerWidth); baseGroup.attr('transform', 'translate(' + posAndPathObj.translateX + ',' + posAndPathObj.translateY + ')'); baseGroup .select('.toolTipPath') .attr({ 'transform': posAndPathObj.translate, d: posAndPathObj.path }) } }); //---------------------------------------------------------------------------------- // MoveTo Helper Functions //---------------------------------------------------------------------------------- /** * getTopLocationsAndPath */ function getTopLocationsAndPath(tooltipPositionObj, containerWidth) { var path; var translate; var translateX; var translateY; translateX = getPathTranslate( tooltipPositionObj.hittingRight, tooltipPositionObj.hittingLeft, (tooltipPositionObj.offsetX + (_Chart.c('caretSize'))), 'width', containerWidth ); translateY = tooltipPositionObj.topY; translate = _Chart._topPath.translate; if (_Chart._moveX !== 0) { path = updateCaretPath('_topPath', 'x'); } else { path = _Chart._topPath.path; } return {translateX: translateX, translateY: translateY, translate: translate, path: path}; } /** * getRightPositionAndPath */ function getRightPositionAndPath(tooltipPositionObj, containerHeight) { var path; var translate; var translateX; var translateY; translateY = getPathTranslate( tooltipPositionObj.hittingBottom, tooltipPositionObj.hittingTop, (tooltipPositionObj.centerY - _Chart.c('padding')), 'height', containerHeight ); translateX = tooltipPositionObj.rightX; translate = _Chart._rightPath.translate; if (_Chart._moveY !== 0) { path = updateCaretPath('_rightPath', 'y'); } else { path = _Chart._rightPath.path; } return {translateX: translateX, translateY: translateY, translate: translate, path: path}; } /** * getBottomPositionAndPath */ function getBottomPositionAndPath(tooltipPositionObj, containerWidth) { var path; var translate; var translateX; var translateY; translateX = getPathTranslate( tooltipPositionObj.hittingRight, tooltipPositionObj.hittingLeft, (tooltipPositionObj.offsetX + (_Chart.c('caretSize') / 2)), 'width', containerWidth ); translateY = (tooltipPositionObj.bottomY + _Chart.c('padding')) + 5; translate = _Chart._bottomPath.translate; if (_Chart._moveX !== 0) { path = updateCaretPath('_bottomPath', 'x'); } else { path = _Chart._bottomPath.path; } return {translateX: translateX, translateY: translateY, translate: translate, path: path}; } /** * getLeftPositionAndPath */ function getLeftPositionAndPath (tooltipPositionObj, containerHeight) { var path; var translate; var translateX; var translateY; translateY = getPathTranslate( tooltipPositionObj.hittingBottom, tooltipPositionObj.hittingTop, (tooltipPositionObj.centerY - _Chart.c('padding')), 'height', containerHeight ); translateX = tooltipPositionObj.leftX; translate = _Chart._leftPath.translate; if (_Chart._moveY !== 0) { path = updateCaretPath('_leftPath', 'y'); } else { path = _Chart._leftPath.path; } return {translateX: translateX, translateY: translateY, translate: translate, path: path}; } /** * Returns the tooltips draw position and path given the current draw location. * * @param location * @param tooltipPositionObj * @param containerHeight * @returns {{translateX: *, translateY: *, translate: *, path: *}} */ function calPositionAndPath(location, tooltipPositionObj, containerHeight, containerWidth) { var positionAndPath; switch (location) { case 'top': positionAndPath = getTopLocationsAndPath(tooltipPositionObj, containerWidth); break; case 'right': positionAndPath = getRightPositionAndPath(tooltipPositionObj, containerHeight); break; case 'bottom': positionAndPath = getBottomPositionAndPath(tooltipPositionObj, containerWidth); break; case 'left': positionAndPath = getLeftPositionAndPath(tooltipPositionObj, containerHeight); break; } return positionAndPath; } /** * Determine the direction the tooltip should draw given its location. * @param tooltipPositionObj * @returns {string} */ function determineInboundsTooltipLocation(tooltipPositionObj) { var location = 'top'; if ((tooltipPositionObj.hittingRight || tooltipPositionObj.hittingLeft) && !tooltipPositionObj.closeToTop) { location = 'top'; } else if ((tooltipPositionObj.hittingRight || tooltipPositionObj.hittingLeft) && tooltipPositionObj.closeToTop) { location = 'bottom'; } else if (tooltipPositionObj.hittingTop && tooltipPositionObj.closeToRight) { location = 'left'; } else if (tooltipPositionObj.hittingTop && tooltipPositionObj.closeToLeft) { location = 'right'; } else if (!tooltipPositionObj.hittingRight && tooltipPositionObj.closeToRight) { location = 'left'; } else if (!tooltipPositionObj.hittingLeft && tooltipPositionObj.closeToLeft) { location = 'right'; } else if (tooltipPositionObj.closeToBottom && tooltipPositionObj.closeToTop) { location = 'top'; } else if (tooltipPositionObj.closeToTop) { location = 'bottom'; } return location; } /** * getTooltipPositionStatus */ function getTooltipPositionStatus(containerWidth, containerHeight, point) { var rectBBox = _Chart._gBBox; var offsetY = point.y - _Chart._baseGroupBBox.height; var offsetX = point.x - _Chart._baseGroupBBox.width / 2; var offsetPadding = _Chart._caretWidth; var topY = offsetY - offsetPadding; var bottomY = point.y + offsetPadding; var centerY = point.y - rectBBox.height / 2; var leftX = point.x - _Chart._baseGroupBBox.width - _Chart.c('caretSize'); var rightX = point.x + offsetPadding + 5; var closeToRight = point.x + _Chart._baseGroupBBox.width / 2 >= containerWidth; var closeToLeft = point.x - _Chart._baseGroupBBox.width / 2 <= 0; var closeToTop = point.y - _Chart._baseGroupBBox.height - offsetPadding - 3 <= 0; var closeToBottom = point.y + _Chart._baseGroupBBox.height > containerHeight; var hittingRight = closeToRight && point.x - _Chart._baseGroupBBox.width - offsetPadding <= 0; var hittingLeft = closeToLeft && point.x + _Chart._baseGroupBBox.width + offsetPadding >= containerWidth; var hittingTop = (closeToRight || closeToLeft) && (point.y - (_Chart._baseGroupBBox.height / 2) <= 0); var hittingBottom = (closeToRight || closeToLeft) && (point.y + (_Chart._baseGroupBBox.height / 2) >= containerHeight); return { offsetX: offsetX, topY: topY, bottomY: bottomY, centerY: centerY, leftX: leftX, rightX: rightX, closeToRight: closeToRight, closeToLeft: closeToLeft, closeToTop: closeToTop, closeToBottom: closeToBottom, hittingRight: hittingRight, hittingLeft: hittingLeft, hittingTop: hittingTop, hittingBottom: hittingBottom }; } /** * SetBaseGroupBBoxDimensions * @param location position of the tooltip */ function setBaseGroupBBoxDimensions(location) { if (_Chart._baseGroupBBox) { _Chart._baseGroupBBox.height = location === 'top' || location === 'bottom' ? _Chart._gBBox.height + (_Chart.c('padding') * 2) + _Chart.c('caretSize') + 6 : _Chart._gBBox.height + (_Chart.c('padding') * 2) + 6; _Chart._baseGroupBBox.width = location === 'top' || location === 'bottom' ? _Chart._gBBox.width + (_Chart.c('padding') * 2) + 6 : _Chart._gBBox.width + (_Chart.c('padding') * 2) + _Chart.c('caretSize') + 6; } } /*---------------------------------------------------------------------------------- Init functions ----------------------------------------------------------------------------------*/ /** * setupMoveToListener */ function setupMoveToListener() { _Chart.base.node().removeEventListener('mousemove', moveToolTip); if (_Chart.c('autoTriggerMoveToEvent')) { _Chart.base.on('mousemove', moveToolTip); } } //---------------------------------------------------------------------------------- // Helper Functions //---------------------------------------------------------------------------------- /** * On MouseMove, move the tooltip. */ function moveToolTip() { var coordinates = [0, 0]; coordinates = d3.mouse(_Chart.base.node()); var x = coordinates[0]; var y = coordinates[1]; var point = { x: x, y: y}; _Chart.trigger('moveTo', point); } /** * Returns if appends is in text rect text format * @param appends * @returns {boolean} */ function isTextRectText(appends) { var returnVal = true; for (var i = 0; i < appends.node().childNodes.length; i++) { var nodeName = appends.node().childNodes[i].nodeName; returnVal = (i % 2 === 0 && nodeName === 'text' ? true : (nodeName === 'rect')); if (!returnVal) { break;} } return returnVal; } /** * Formats appends as text rect text * @param appends * @param groupBBox */ function formatAppendTextRectText(appends) { var padding = 5; var prevWidth = 0; var groupBBox = {x:0,y:0,height:0,width:0}; appends.select('rect').attr('height', 0); groupBBox = appends.node().getBBox(); /*jshint -W083 */ for (var i = 0; i < appends.node().childNodes.length; i++) { var element = appends.node().childNodes[i]; if (i % 2 === 0) { //text var t = appends.select('text:nth-child(' + (i + 1) + ')') .attr({ dy: function () {return getCentralPosition(this);}, x: (prevWidth + (padding * i) + 1), y: groupBBox.height / 2 }); prevWidth += element.getBBox().width; } else { //rect appends.select('rect:nth-child(' + (i + 1) + ')') .attr({ x: (prevWidth + (padding * i)), y: 0, width: 1, height: groupBBox.height }); } } appends.selectAll('tspan') .attr({ transform: function () { return 'translate(0, ' + getCentralPosition(this) + ')'; }, y: groupBBox.height / 2 }); } /** * setBaseGroupBBox */ function setBaseGroupBBox() { _Chart._baseGroupBBox = cloneBBox(_Chart._baseGroup.node().getBBox()); _Chart._baseGroupBBox.height = _Chart._baseGroupBBox.height - _Chart._caretWidth + 3; } /** * Set gBBox's height and width to height, width if defined else * it will use appends BBox. * @param height * @param width */ function setGBBox(height, width) { if (height !== undefined && width !== undefined) { _Chart._gBBox = { 'height': height, 'width': width }; } else { _Chart._gBBox = _Chart._appends.node().getBBox(); } } /** * Calculates vertical center for text * @param elem * @returns {number} */ function getCentralPosition(elem) { return parseInt(d3.select(elem) .style('font-size'), 10) * 0.35; } /** * Returns a tranlate that keeps the path in bounds. * @param hittingEdge1 * @param hittingEdge2 * @param offset * @param dimension * @param heightOrWidth * @returns {*} */ function getPathTranslate(hittingEdge1, hittingEdge2, offset, dimension, heightOrWidth) { var translate; if (hittingEdge1) { translate = heightOrWidth - _Chart._baseGroupBBox[dimension] + r; } else if (hittingEdge2) { translate = r; } else { translate = offset; } return translate; } /** * UpdateCaretPath * @param path * @param axis * @returns {.autoshot.default_options.options.path|*|.autoshot.default_options.options.local.path|options.path|options.local.path|path} */ function updateCaretPath(path, axis) { var pathString; var xyString; if (axis === 'y') { xyString = _Chart[path].caretX + ' ' + (_Chart[path].caretY - _Chart._moveY); } else { xyString = (_Chart[path].caretX + _Chart._moveX) + ' ' + _Chart[path].caretY; } pathString = _Chart[path].path.match(/Z m (.*?) l/); _Chart[path].path = _Chart[path].path.replace(pathString[1], xyString); return _Chart[path].path; } /** * OffsetCaretAtEdge * @param config */ function offsetCaretAtEdge(config) { var container = config.container; var point = config.point; var tooltipWidth; var tooltipHeight; var bboxWidth; var bboxHeight; var mouseXOffset = container.width - point.x; var mouseYOffset = point.y; var curvedRectOffset = 2*r; //Compensate for the curved rect path. 1 is for good measure. bboxHeight = _Chart._gBBox.height// - curvedRectOffset; bboxWidth = _Chart._gBBox.width// - curvedRectOffset; var farthestCaretPosition; if (config.left || config.right) { //Left if (config.left) { tooltipWidth = container.width - (_Chart._baseGroupBBox.width / 2); _Chart._moveX = tooltipWidth - mouseXOffset; if ((bboxWidth / 2) + _Chart.c('caretSize') <= _Chart._moveX) { _Chart._moveX = -((bboxWidth / 2) + _Chart.c('caretSize')); } } //Right if (config.right) { tooltipWidth = _Chart._baseGroupBBox.width / 2; _Chart._moveX = tooltipWidth - mouseXOffset; if (-((bboxWidth / 2) + _Chart.c('caretSize')) >= _Chart._moveX) { _Chart._moveX = -((bboxWidth / 2) + _Chart.c('caretSize')); } } } else if (config.top || config.bottom) { //Top if (config.top) { tooltipHeight = _Chart._baseGroupBBox.height / 2; _Chart._moveY = tooltipHeight - mouseYOffset; farthestCaretPosition = (bboxHeight / 2) + _Chart.c('caretSize') - curvedRectOffset; if (farthestCaretPosition <= _Chart._moveY) { _Chart._moveY = farthestCaretPosition; } } //Bottom if (config.bottom) { tooltipHeight = container.height - ((_Chart._baseGroupBBox.height) / 2); _Chart._moveY = tooltipHeight - mouseYOffset; farthestCaretPosition = -(((bboxHeight) / 2) - _Chart.c('caretSize')) - curvedRectOffset; if (farthestCaretPosition >= _Chart._moveY) { _Chart._moveY = farthestCaretPosition; } } } else { _Chart._moveX = 0; _Chart._moveY = 0; } } /** * GeneratePaths */ function generatePaths() { var w = _Chart._gBBox.width + (_Chart.c('padding') * 2); var h = _Chart._gBBox.height + (_Chart.c('padding') * 2); var c = _Chart.c('caretSize'); var y = (h / 2) + _Chart.c('caretSize') + r; _Chart._topPath = { translate: 'translate(' + (w / 2) + ',' + (h + c + r) + ')', caretX: ((w / 2) - c), caretY: (h + (r * 2)) }; _Chart._topPath.path = 'm -' + (w / 2) + ' -' + (h + c + (r * 2)) + ' h ' + w + ' c 1.7 0 ' + r + ' 1.3 ' + r + ' ' + r + ' v ' + h + ' c 0 1.7 -1.3 ' + r + ' -' + r + ' ' + r + ' h -' + w + ' c -1.7 0 -' + r + ' -1.3 -' + r + ' -' + r + ' v -' + h + ' c 0 -1.7 1.3 -' + r + ' ' + r + ' -' + r + ' Z m ' + _Chart._topPath.caretX + ' ' + _Chart._topPath.caretY + ' l ' + c + ' ' + c + ' l ' + c + ' -' + c; _Chart._bottomPath = { translate: 'translate(' + (w / 2) + ',' + (-c + (r / 2)) + ')', caretX: ((w / 2) - c), caretY: 0 }; _Chart._bottomPath.path = 'm -' + (w / 2) + ' ' + 0 + ' h ' + w + ' c 1.7 0 ' + r + ' 1.3 ' + r + ' ' + r + ' v ' + h + ' c 0 1.7 -1.3 ' + r + ' -' + r + ' ' + r + ' h -' + w + ' c -1.7 0 -' + r + ' -1.3 -' + r + ' -' + r + ' v -' + h + ' c 0 -1.7 1.3 -' + r + ' ' + r + ' -' + r + ' Z m ' + _Chart._bottomPath.caretX + ' ' + _Chart._bottomPath.caretY + ' l ' + c + ' -' + c + ' l ' + c + ' ' + c; _Chart._leftPath = { translate: 'translate(' + (w + c + r) + ',' + (h / 2) + ')', caretX: (w + r), caretY: y }; _Chart._leftPath.path = 'm -' + (w + c + r) + ' ' + -((h / 2) + r) + ' h ' + w + ' c 1.7 0 ' + r + ' 1.3 ' + r + ' ' + r + ' v ' + h + ' c 0 1.7 -1.3 ' + r + ' -' + r + ' ' + r + ' h -' + w + ' c -1.7 0 -' + r + ' -1.3 -' + r + ' -' + r + ' v -' + h + ' c 0 -1.7 1.3 -' + r + ' ' + r + ' -' + r + ' Z m ' + _Chart._leftPath.caretX + ' ' + _Chart._leftPath.caretY + ' l ' + c + ' -' + c + ' l -' + c + ' -' + c; _Chart._rightPath = { translate: 'translate(' + -(c + r) + ',' + (h / 2) + ')', caretX: -r, caretY: y }; _Chart._rightPath.path = 'm ' + (c + r) + ' ' + -((h / 2) + r) + ' h ' + w + ' c 1.7 0 ' + r + ' 1.3 ' + r + ' ' + r + ' v ' + h + ' c 0 1.7 -1.3 ' + r + ' -' + r + ' ' + r + ' h -' + w + ' c -1.7 0 -' + r + ' -1.3 -' + r + ' -' + r + ' v -' + h + ' c 0 -1.7 1.3 -' + r + ' ' + r + ' -' + r + ' Z m ' + _Chart._rightPath.caretX + ' ' + _Chart._rightPath.caretY + ' l -' + c + ' -' + c + ' l ' + c + ' -' + c + ' Z'; } /** * cloneBBox, IE will not let you modify the obj returned from getBBox * @param box * @returns {{x: *, y: *, width: *, height: *}} */ function cloneBBox(box) { return { x: box.x, y: box.y, width: box.width, height: box.height }; } } });