// o---------------------------------------------------------------------------------o // | This file is part of the RGraph package - you can learn more at: | // | | // | https://www.rgraph.net/license.html | // | | // | RGraph is dual-licensed under the Open Source GPL license. That means that it's | // | free to use and there are no restrictions on what you can use RGraph for! | // | If the GPL license does not suit you however, then there's an inexpensive | // | commercial license option available. See the URL above for more details. | // o---------------------------------------------------------------------------------o RGraph = window.RGraph || {isrgraph:true,isRGraph:true,rgraph:true}; RGraph.SVG = RGraph.SVG || {}; // Module pattern (function (win, doc, undefined) { // // This is used in two functions, hence it's here // RGraph.SVG.tooltips = {}; RGraph.SVG.tooltips.css = RGraph.SVG.tooltips.style = { display: 'inline-block', position: 'absolute', padding: '6px', lineHeight: 'initial', fontFamily: 'Arial', fontSize: '12pt', fontWeight: 'normal', textAlign: 'center', left: 0, top: 0, backgroundColor: 'black', color: 'white', visibility: 'visible', zIndex: 3, borderRadius: '5px', boxShadow: 'rgba(96,96,96,0.5) 0 0 5px', transition: 'left ease-out .25s, top ease-out .25s' }; // // Shows a tooltip // // @param obj The chart object // @param opt The options // RGraph.SVG.tooltip = function (opt) { var obj = opt.object; var properties = obj.properties; // Fire the beforetooltip event RGraph.SVG.fireCustomEvent(obj, 'onbeforetooltip'); if (!opt.text || typeof opt.text === 'undefined' || RGraph.SVG.trim(opt.text).length === 0) { if (typeof properties.tooltipsOverride !== 'function') { return; } } // // The tooltipsOverride option allows you to totally take control of // rendering the tooltip yourself. // if (RGraph.SVG.isFunction(properties.tooltipsOverride)) { // Add the body click handler that clears the highlight if necessary // document.body.addEventListener('mouseup', function (e) { obj.removeHighlight(); }, false); return (properties.tooltipsOverride)(obj, opt); } opt.object.removeHighlight(); // Create the tooltip DIV element if (!RGraph.SVG.REG.get('tooltip')) { var tooltipObj = document.createElement('DIV'); tooltipObj.className = properties.tooltipsCssClass; // Add the default CSS to the tooltip for (var i in RGraph.SVG.tooltips.style) { if (typeof i === 'string') { tooltipObj.style[i] = substitute(RGraph.SVG.tooltips.style[i]); } } for (var i in RGraph.SVG.tooltips.css) { if (typeof i === 'string') { tooltipObj.style[i] = substitute(RGraph.SVG.tooltips.css[i]); } } // // If the tooltipsCss property is populated the add those values // to the tooltip // if (!RGraph.SVG.isNullish(obj.properties.tooltipsCss)) { for (var i in obj.properties.tooltipsCss) { if (typeof i === 'string') { tooltipObj.style[i] = substitute(obj.properties.tooltipsCss[i]); } } } // Reuse an existing tooltip } else { var tooltipObj = RGraph.SVG.REG.get('tooltip'); tooltipObj.__object__.removeHighlight(); // This prevents the object from continuously growing tooltipObj.style.width = ''; } if (RGraph.SVG.REG.get('tooltip-lasty')) { tooltipObj.style.left = RGraph.SVG.REG.get('tooltip-lastx') + 'px'; tooltipObj.style.top = RGraph.SVG.REG.get('tooltip-lasty') + 'px'; } /////////////////////////////////////// // Do tooltip text substitution here // /////////////////////////////////////// function substitute (original) { // Ensure that it's a string first original = String(original); if (typeof opt.object.tooltipSubstitutions !== 'function') { return original; } // Get hold of the indexes from the sequentialIndex that we have. // if (typeof opt.object.tooltipSubstitutions === 'function') { var specific = opt.object.tooltipSubstitutions({ index: opt.sequentialIndex }); } // This allows for escaping the percent var text = original.replace(/%%/g, '___--PERCENT--___') // // Draws the key in the tooltip // var keyReplacementFunction = function () { if (!specific.values) { return; } // // Allow the user to specify the key colors // var colors = properties.tooltipsFormattedKeyColors ? properties.tooltipsFormattedKeyColors : properties.colors; if (!colors) { colors = [properties.colorsDefault]; } // Build up the HTML table that becomes the key for (var i=0,str=[]; i<specific.values.length; ++i) { var value = (typeof specific.values === 'object' && typeof specific.values[i] === 'number') ? specific.values[i] : 0; var color = colors[i]; var label = ( (typeof properties.tooltipsFormattedKeyLabels === 'object' && typeof properties.tooltipsFormattedKeyLabels[i] === 'string') ? properties.tooltipsFormattedKeyLabels[i] : ''); // Chart specific customisations ------------------------- if (typeof opt.object.tooltipsFormattedCustom === 'function') { // The index/group/sequential index // The index // The colors var ret = opt.object.tooltipsFormattedCustom( specific, i, colors ); if (ret.continue) {continue;}; if (typeof ret.label === 'string') {label = ret.label;}; if (ret.color) {color = ret.color;}; if (typeof ret.value === 'number') {value = ret.value;}; } value = RGraph.SVG.numberFormat({ object: opt.object, num: value.toFixed(opt.object.properties.tooltipsFormattedDecimals), thousand: opt.object.properties.tooltipsFormattedThousand || ',', point: opt.object.properties.tooltipsFormattedPoint || '.', prepend: opt.object.properties.tooltipsFormattedUnitsPre || '', append: opt.object.properties.tooltipsFormattedUnitsPost || '' }); // If the tooltipsFormattedKeyColorsShape property is set to circle then add // some border-radius to the DIV tag // var borderRadius = 0; if ( typeof opt.object.properties.tooltipsFormattedKeyColorsShape === 'string' && opt.object.properties.tooltipsFormattedKeyColorsShape === 'circle') { borderRadius = '100px'; } // Facilitate the property that allows CSS to be added to // the tooltip key color blob var tooltipsFormattedKeyColorsCss = ''; if (properties.tooltipsFormattedKeyColorsCss) { for(property in properties.tooltipsFormattedKeyColorsCss) { if (typeof property === 'string') { tooltipsFormattedKeyColorsCss += '{1}: {2};'.format(property.replace(/[A-Z]/, function (match) { return '-' + match.toLowerCase(); }), String(properties.tooltipsFormattedKeyColorsCss[property])); } } } str[i] = '<tr><td><div class="RGraph_tooltipsFormattedKeyColor" id="RGraph_tooltipsFormattedKeyColor_' + i + '" ' + '>Ml</div></td><td>' + '<span id="RGraph_tooltipsFormattedKeyLabel_' + i + '">' + label + '</span>' + ' ' + value + '</td></tr>'; // Now that styles can't be applied inline // (due to the CSP header) then apply them with // JavaScript after a small delay. (function (index, color, borderRadius) { setTimeout(function () { // Align the label left var node = document.getElementById('RGraph_tooltipsFormattedKeyLabel_' + index); if (node) { node.style.textAlign = 'left' } // Add some styles to the color blob var colorBlob = document.getElementById('RGraph_tooltipsFormattedKeyColor_' + index); if (colorBlob) { colorBlob.style.textAlign = 'left'; colorBlob.style.backgroundColor = color; colorBlob.style.color = 'transparent'; colorBlob.style.pointerEvents = 'none'; colorBlob.style.borderRadius = borderRadius; // Add user specified styles from the // tooltipsFormattedKeyColorsCss property for (var property in tooltipsFormattedKeyColorsCss) { if (typeof property === 'string') { colorBlob.style[property] = tooltipsFormattedKeyColorsCss[property]; } } } // Set the default color for the table to inherit var node = document.getElementById('RGraph_tooltipsFormattedKey_table') if (node) { node.style.color = 'inherit'; } }, 1); })(i, color, borderRadius); } str = str.join(''); // Add the key to the tooltip text - replacing the placeholder text = text.replace('%{key}', '<table id="RGraph_tooltipsFormattedKey_table">' + str + '</table>'); }; keyReplacementFunction(); // Replace the index of the tooltip text = text.replace(/%{index}/g, specific.index); // Replace the dataset/group of the tooltip text = text.replace(/%{dataset2}/g, specific.dataset2); // Used by the Bipolar text = text.replace(/%{dataset}/g, specific.dataset); text = text.replace(/%{group2}/g, specific.dataset2); // Used by the Bipolar text = text.replace(/%{group}/g, specific.dataset); // Replace the sequentialIndex of the tooltip text = text.replace(/%{sequential_index}/g, specific.sequentialIndex); text = text.replace(/%{seq}/g, specific.sequentialIndex); //Do %{list} sunstitution if (text.indexOf('%{list}') !== -1) { (function () { if (properties.tooltipsFormattedListType === 'unordered') properties.tooltipsFormattedListType = 'ul'; if (properties.tooltipsFormattedListType === '<ul>') properties.tooltipsFormattedListType = 'ul'; if (properties.tooltipsFormattedListType === 'ordered') properties.tooltipsFormattedListType = 'ol'; if (properties.tooltipsFormattedListType === '<ol>') properties.tooltipsFormattedListType = 'ol'; var str = properties.tooltipsFormattedListType === 'ol' ? '<ol id="rgraph_formatted_tooltips_list">' : '<ul id="rgraph_formatted_tooltips_list">'; var items = properties.tooltipsFormattedListItems[specific.sequentialIndex]; if (items && items.length) { for (var i=0; i<items.length; ++i) { str += '<li>' + items[i] + '</li>'; } } str += properties.tooltipsFormattedListType === 'ol' ? '</ol>' : '</ul>'; // Add the list to the tooltip text = text.replace(/%{list}/, str); })(); } // Do table substitution (ie %{table} ) if (text.indexOf('%{table}') !== -1) { (function () { var str = '<table>'; // Add the headers if they're defined if (properties.tooltipsFormattedTableHeaders && properties.tooltipsFormattedTableHeaders.length) { str += '<thead><tr>'; for (var i=0; i<properties.tooltipsFormattedTableHeaders.length; ++i) { str += '<th>' + properties.tooltipsFormattedTableHeaders[i] + '</th>'; } str += '</tr></thead>'; } // Add each row of data if (typeof properties.tooltipsFormattedTableData === 'object' && !RGraph.SVG.isNullish(properties.tooltipsFormattedTableData)) { str += '<tbody>'; for (var i=0; i<properties.tooltipsFormattedTableData[specific.sequentialIndex].length; ++i) { str += '<tr>'; for (var j=0; j<properties.tooltipsFormattedTableData[specific.sequentialIndex][i].length; ++j) { str += '<td>' + String(properties.tooltipsFormattedTableData[specific.sequentialIndex][i][j]) + '</td>'; } str += '</tr>'; } str += '</tbody>'; } // Close the table str += '</table>'; text = text.replace(/%{table}/g, str); })(); } // Do property substitution when there's an index to // the property var reg = /%{pr?o?p?(?:erty)?:([_a-z0-9]+)\[([0-9]+)\]}/i; while (text.match(reg)) { var property = RegExp.$1; var index = parseInt(RegExp.$2); if (opt.object.properties[property]) { text = text.replace( reg, opt.object.properties[property][index] || '' ); // Get rid of the text } else { text = text.replace(reg,''); } RegExp.lastIndex = null; } // Third, replace this: %{property:xxx} (but there's no index to the property) while (text.match(/%{property:([_a-z0-9]+)}/i)) { var str = '%{property:' + RegExp.$1 + '}'; text = text.replace(str, opt.object.properties[RegExp.$1]); } // Fourth, replace this: %%prop:xxx%% while (text.match(/%{prop:([_a-z0-9]+)}/i)) { var str = '%{prop:' + RegExp.$1 + '}'; text = text.replace(str, opt.object.properties[RegExp.$1]); } // Fourth, replace this: %%p:xxx%% while (text.match(/%{p:([_a-z0-9]+)}/i)) { var str = '%{p:' + RegExp.$1 + '}'; text = text.replace(str, opt.object.properties[RegExp.$1]); } // THIS IS ONLY FOR A NON-EQUI-ANGULAR ROSE CHART // // Replace this: %{value2} if (opt.object.type === 'rose' && opt.object.properties.variant === 'non-equi-angular') { while (text.match(/%{value2}/i)) { text = text.replace('%{value2}', specific.value2); } } // Fifth and sixth, replace this: %{value} and this: %{value_formatted} while (text.match(/%{value(?:_formatted)?}/i)) { var value = specific.value; // // Special case for the Waterfall chart and mid totals // if (opt.object.type === 'waterfall' && specific.index != opt.object.data.length - 1 && RGraph.SVG.isNullish(value)) { for (var i=0,tot=0; i<specific.index; ++i) { tot += opt.object.data[i]; } value = tot; } if (text.match(/%{value_formatted}/i)) { text = text.replace( '%{value_formatted}', typeof value === 'number' ? RGraph.SVG.numberFormat({ object: opt.object, num: value.toFixed(opt.object.properties.tooltipsFormattedDecimals), thousand: opt.object.properties.tooltipsFormattedThousand || ',', point: opt.object.properties.tooltipsFormattedPoint || '.', prepend: opt.object.properties.tooltipsFormattedUnitsPre || '', append: opt.object.properties.tooltipsFormattedUnitsPost || '' }) : null ); } else { text = text.replace('%{value}', value); } } //////////////////////////////////////////////////////////////// // Do global substitution when there's an index to the global // //////////////////////////////////////////////////////////////// var reg = /%{global:([_a-z0-9.]+)\[([0-9]+)\]}/i; while (text.match(reg)) { var name = RegExp.$1, index = parseInt(RegExp.$2); if (eval(name)) { text = text.replace( reg, eval(name)[index] || '' ); // Get rid of the text if there was nothing to replace the template bit with } else { text = text.replace(reg,''); } RegExp.lastIndex = null; } ////////////////////////////////////////////////// // Do global substitution when there's no index // ////////////////////////////////////////////////// var reg = /%{global:([_a-z0-9.]+)}/i; while (text.match(reg)) { var name = RegExp.$1; if (eval(name)) { text = text.replace( reg, eval(name) || '' ); // Get rid of the text if there was nothing to replace the template bit with } else { text = text.replace(reg,''); } RegExp.lastIndex = null; } // And lastly - call any functions // MUST be last var regexp = /%{function:([_A-Za-z0-9]+)\((.*?)\)}/; // Temporarily replace carriage returns and line feeds with CR and LF // so the the s option is not needed text = text.replace(/\r/,'|CR|'); text = text.replace(/\n/,'|LF|'); while (text.match(regexp)) { var str = RegExp.$1 + '(' + RegExp.$2 + ')'; for (var i=0,len=str.length; i<len; ++i) { str = str.replace(/\r?\n/, "\\n"); } RGraph.SVG.REG.set('tooltip-templates-function-object', opt.object); var func = new Function ('return ' + str); var ret = func(); text = text.replace(regexp, ret) } // Do color blob replacement text = text.replace(/%{color\:([^}]+)}/g, '<div style="display: inline-block; background-color: $1; scale: 0.9;color: transparent">Mj</div>'); // Replace CR and LF with a space text = text.replace(/\|CR\|/, ' '); text = text.replace(/\|LF\|/, ' '); // Replace line returns with br tags text = text.replace(/\r?\n/g, '<br />'); text = text.replace(/___--PERCENT--___/g, '%') return text.toString(); } // Save the original text on the tooltip tooltipObj.__original_text__ = opt.text; opt.text = substitute(opt.text); // Add the pointer if requested. The background color is updated to match the // tooltip a further down. if (opt.object.properties.tooltipsPointer) { opt.text += '<div id="RGraph_tooltipsPointer_' + opt.object.id + '"></div>'; } tooltipObj.innerHTML = opt.text; tooltipObj.__text__ = opt.text; // This is set because the innerHTML can change when it's set tooltipObj.id = '__rgraph_tooltip_' + obj.id + '_' + obj.uid + '_'+ opt.index; tooltipObj.__event__ = properties.tooltipsEvent || 'click'; tooltipObj.__object__ = obj; // Add the index if (typeof opt.index === 'number') { tooltipObj.__index__ = opt.index; } // Add the dataset if (typeof opt.dataset === 'number') { tooltipObj.__dataset__ = opt.dataset; } // Add the group if (typeof opt.group === 'number' || RGraph.SVG.isNullish(opt.group)) { tooltipObj.__group__ = opt.group; } // Add the sequentialIndex if (typeof opt.sequentialIndex === 'number') { tooltipObj.__sequentialIndex__ = opt.sequentialIndex; } // Add the tooltip to the document document.body.appendChild(tooltipObj); // Set styles on the pointer. It's done this way // (not adding the style to the HTML above) to // prevent an error bein thrown should a // Content-Security-Policy header using style-src // be in place var pointerObj = document.getElementById('RGraph_tooltipsPointer_' + opt.object.id + ''); if (pointerObj) { var styles = window.getComputedStyle(tooltipObj, false); pointerObj.style.backgroundColor = styles.backgroundColor; pointerObj.style.color = 'transparent'; pointerObj.style.position = 'absolute'; pointerObj.style.bottom = -5 + (RGraph.SVG.isNumber(obj.properties.tooltipsPointerOffsety) ? obj.properties.tooltipsPointerOffsety : 0) + 'px'; pointerObj.style.left = 'calc(50% + ' + (RGraph.SVG.isNumber(obj.properties.tooltipsPointerOffsetx) ? obj.properties.tooltipsPointerOffsetx : 0) + 'px)'; pointerObj.style.transform = 'translateX(-50%) rotate(45deg)'; pointerObj.style.width = '10px'; pointerObj.style.height = '10px'; } var width = tooltipObj.offsetWidth, height = tooltipObj.offsetHeight; // Set these properties to 0 (ie an integer) in case chart libraries are missing // default values for them obj.properties.tooltipsOffsetx = obj.properties.tooltipsOffsetx || 0; obj.properties.tooltipsOffsety = obj.properties.tooltipsOffsety || 0; // Move the tooltip into position tooltipObj.style.left = opt.event.pageX - (width / 2) + obj.properties.tooltipsOffsetx + 'px'; // Prevent the top of the tooltip from being placed off the top of the page var y = opt.event.pageY - height - 15; if (y < 0) { y = 5; } tooltipObj.style.top = y + obj.properties.tooltipsOffsety + 'px'; // // Set the width on the tooltip so it doesn't resize if the window is resized // tooltipObj.style.width = width + 'px'; // // Now that the tooltip pointer has been added, determine the background-color and update // the color of the pointer if (opt.object.properties.tooltipsPointer) { var styles = window.getComputedStyle(tooltipObj, false); var pointer = document.getElementById('RGraph_tooltipsPointer_' + opt.object.id + ''); pointer.style.backgroundColor = styles.backgroundColor; // Add the pointer to the tooltip as a property tooltipObj.__pointer__ = pointer; // Facilitate the property that allows CSS to be added to // the tooltip key color blob var tooltipsPointerCss = ''; if (opt.object.properties.tooltipsPointerCss) { var pointerDiv = document.getElementById('RGraph_tooltipsPointer_' + opt.object.id + ''); for(property in opt.object.properties.tooltipsPointerCss) { if (typeof property === 'string') { pointerDiv.style[property] = opt.object.properties.tooltipsPointerCss[property]; } } } } // Fade the tooltip in if the tooltip is the first view //if (!RGraph.SVG.REG.get('tooltip-lastx')) { // for (var i=0; i<=30; ++i) { // (function (idx) // { // setTimeout(function () // { // tooltipObj.style.opacity = (idx / 30) * 1; // }, (idx / 30) * 200); // })(i); // } //} // If the left is less than zero - set it to 5 if (parseFloat(tooltipObj.style.left) <= 5) { tooltipObj.style.left = 5 + obj.properties.tooltipsOffsetx + 'px'; } // If the tooltip goes over the right hand edge then // adjust the positioning if (parseFloat(tooltipObj.style.left) + parseFloat(tooltipObj.style.width) > window.innerWidth) { tooltipObj.style.left = '' tooltipObj.style.right = 5 + obj.properties.tooltipsOffsety + 'px' } // // Allow for static positioning. // if (opt.object.properties.tooltipsPositionStatic && RGraph.SVG.isFunction(opt.object.positionTooltipStatic)) { opt.object.positionTooltipStatic({ object: opt.object, event: opt.event, tooltip: tooltipObj, index: tooltipObj.__sequentialIndex__ }); // Allow for offsetting the position tooltipObj.style.left = parseFloat(tooltipObj.style.left) + obj.properties.tooltipsOffsetx + 'px'; tooltipObj.style.top = parseFloat(tooltipObj.style.top) + obj.properties.tooltipsOffsety + 'px'; } // // Move the tooltip and its pointer ifthey're off-screen LHS // if (parseInt(tooltipObj.style.left) < 0) { var left = parseInt(tooltipObj.style.left); var width = parseInt(tooltipObj.style.width) left = left + (width * 0.1 * 4); tooltipObj.style.left = left + 'px'; var pointer = document.getElementById('RGraph_tooltipsPointer_' + opt.object.id + ''); if (pointer) { (function (pointer) { setTimeout(function () { pointer.style.left = 'calc(10% + 5px)'; }, 1); })(pointer); } // // Move the tooltip and its pointer ifthey're off-screen RHS // } else if ( (parseInt(tooltipObj.style.left) + parseInt(tooltipObj.offsetWidth)) > document.body.offsetWidth) { var left = parseInt(tooltipObj.style.left); var width = parseInt(tooltipObj.style.width) left = left - (width * 0.1 * 4); tooltipObj.style.left = left + 'px'; var pointer = document.getElementById('RGraph_tooltipsPointer_' + opt.object.id + ''); (function (pointer) { setTimeout(function () { if (pointer) { pointer.style.left = 'calc(90% - 5px)'; } }, 1); })(pointer); } // If the canvas has fixed positioning then set the tooltip position to // fixed too if (RGraph.SVG.isFixed(obj.svg)) { var scrollTop = window.scrollY || document.documentElement.scrollTop; tooltipObj.style.position = 'fixed'; tooltipObj.style.top = opt.event.pageY - scrollTop - height - 10 + obj.properties.tooltipsOffsety + 'px'; } // Cancel the mousedown event tooltipObj.onmousedown = function (e) { e.stopPropagation(); }; // Cancel the mouseup event tooltipObj.onmouseup = function (e) { e.stopPropagation(); }; // Cancel the click event tooltipObj.onclick = function (e) { if (e.button == 0) { e.stopPropagation(); } }; // Add the body click handler that clears the tooltip document.body.addEventListener('mouseup', function (e) { RGraph.SVG.hideTooltip(); }, false); // // Keep a reference to the tooltip in the registry // RGraph.SVG.REG.set('tooltip', tooltipObj); RGraph.SVG.REG.set('tooltip-lastx', parseFloat(tooltipObj.style.left)); RGraph.SVG.REG.set('tooltip-lasty', parseFloat(tooltipObj.style.top)); ///////////////////////// // PERSISTENT tooltips // ///////////////////////// // // Enable persistent tooltips if requested // if (opt.object.get('tooltipsPersistent')) { if (!RGraph.SVG.tooltip.persistent) { RGraph.SVG.tooltip.persistent = {divs:[]}; } setTimeout(function () { // Change the pointer ID var pointer = document.getElementById('RGraph_tooltipsPointer_' + opt.object.id); if (pointer) { pointer.id += '_' + RGraph.SVG.random(18564, 999999); } // Save a reference to all of the tooltip DIV // objects RGraph.SVG.tooltip.persistent.divs.push(RGraph.SVG.REG.get('tooltip')); // Set the tooltips reference to null RGraph.SVG.REG.set('tooltip', null); // Set the last coords (for the slide effect) // to null //RGraph.tooltip_slide_effect_previous_x_coordinate = null; //RGraph.tooltip_slide_effect_previous_y_coordinate = null; // }, 50); // // A function to clear all of the persistent // tooltip DIV tags // RGraph.SVG.tooltip.persistent.clear = RGraph.SVG.tooltip.persistent.clearAll = function () { var els = document.getElementsByClassName(opt.object.get('tooltipsCSSClass')); // Create a real array - not an HTMLCollection var arr = Array.from(els); for (let i=0,len=arr.length; i<len; ++i) { arr[i].parentNode.removeChild(arr[i]); } RGraph.SVG.redraw(); } } ///////////////// ///////////////// ///////////////// // // Fire the tooltip event // RGraph.SVG.fireCustomEvent(obj, 'ontooltip'); }; // End module pattern })(window, document);