// version: 2019-05-27 /** * o--------------------------------------------------------------------------------o * | This file is part of the RGraph package - you can learn more at: | * | | * | https://www.rgraph.net | * | | * | RGraph is licensed under the Open Source MIT license. That means that it's | * | totally free to use and there are no restrictions on what you can do with it! | * o--------------------------------------------------------------------------------o */ // modified: 2019-06-16 by WBB // make the behavior of the Y-axis consistent with that of the X-axis // draw the Y-axis at the origin, if within the chart X-range, otherwise draw it at the left margin RGraph = window.RGraph || {isRGraph: true,isRGraphSVG: true}; RGraph.SVG = RGraph.SVG || {}; RGraph.SVG.FX = RGraph.SVG.FX || {}; // Module pattern (function (win, doc, undefined) { var RG = RGraph, ua = navigator.userAgent, ma = Math; RG.SVG.REG = { store: [] }; // ObjectRegistry RG.SVG.OR = {objects: []}; // Used to categorise trigonometery functions RG.SVG.TRIG = {}; RG.SVG.TRIG.HALFPI = ma.PI * .4999; RG.SVG.TRIG.PI = RG.SVG.TRIG.HALFPI * 2; RG.SVG.TRIG.TWOPI = RG.SVG.TRIG.PI * 2; RG.SVG.ISIE = ua.indexOf('rident') > 0; RG.SVG.ISFF = ua.indexOf('irefox') > 0; RG.SVG.events = []; // This allows you to set globalconfiguration values that are copied to // all objects automatically. RG.SVG.GLOBALS = {}; RG.SVG.ISFF = ua.indexOf('Firefox') != -1; RG.SVG.ISOPERA = ua.indexOf('Opera') != -1; RG.SVG.ISCHROME = ua.indexOf('Chrome') != -1; RG.SVG.ISSAFARI = ua.indexOf('Safari') != -1 && !RG.SVG.ISCHROME; RG.SVG.ISWEBKIT = ua.indexOf('WebKit') != -1; RG.SVG.ISIE = ua.indexOf('Trident') > 0 || navigator.userAgent.indexOf('MSIE') > 0; RG.SVG.ISIE6 = ua.indexOf('MSIE 6') > 0; RG.SVG.ISIE7 = ua.indexOf('MSIE 7') > 0; RG.SVG.ISIE8 = ua.indexOf('MSIE 8') > 0; RG.SVG.ISIE9 = ua.indexOf('MSIE 9') > 0; RG.SVG.ISIE10 = ua.indexOf('MSIE 10') > 0; RG.SVG.ISIE11UP = ua.indexOf('MSIE') == -1 && ua.indexOf('Trident') > 0; RG.SVG.ISIE10UP = RG.SVG.ISIE10 || RG.SVG.ISIE11UP; RG.SVG.ISIE9UP = RG.SVG.ISIE9 || RG.SVG.ISIE10UP; // // Create an SVG tag // RG.SVG.createSVG = function (opt) { var container = opt.container, obj = opt.object; if (container.__svg__) { return container.__svg__; } var svg = doc.createElementNS("http://www.w3.org/2000/svg", "svg"); svg.setAttribute('style', 'top: 0; left: 0; position: absolute'); svg.setAttribute('width', container.offsetWidth); svg.setAttribute('height', container.offsetHeight); svg.setAttribute('version', '1.1'); svg.setAttributeNS("http://www.w3.org/2000/xmlns/", 'xmlns', 'http://www.w3.org/2000/svg'); svg.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:xlink", "http://www.w3.org/1999/xlink"); svg.__object__ = obj; svg.__container__ = container; container.appendChild(svg); container.__svg__ = svg; container.__object__ = obj; var style = getComputedStyle(container); if (style.position !== 'absolute' && style.position !== 'fixed' && style.position !== 'sticky') { container.style.position = 'relative'; } // Add the groups that facilitate "background layers" var numLayers = 10; for (var i=1; i<=numLayers; ++i) { var group = RG.SVG.create({ svg: svg, type: 'g', attr: { className: 'background' + i } }); // Store a reference to the group obj.layers['background' + i] = group; svg['background' + i] = group; } // Add the group tag to the SVG that contains all of the elements var group = RG.SVG.create({ svg: svg, type: 'g', attr: { className: 'all-elements' } }); container.__svg__.all = group; return svg; }; // // Create a defs tag inside the SVG // RG.SVG.createDefs = function (obj) { if (!obj.svg.defs) { var defs = RG.SVG.create({ svg: obj.svg, type: 'defs' }); obj.svg.defs = defs; } return defs; }; // // Creates a tag depending on the args that you give // //@param opt object The options for the function // RG.SVG.create = function (opt) { var ns = "http://www.w3.org/2000/svg", tag = doc.createElementNS(ns, opt.type); // Add the attributes for (var o in opt.attr) { if (typeof o === 'string') { var name = o; if (o === 'className') { name = 'class'; } if ( (opt.type === 'a' || opt.type === 'image') && o === 'xlink:href') { tag.setAttributeNS('http://www.w3.org/1999/xlink', o, String(opt.attr[o])); } else { if (RG.SVG.isNull(opt.attr[o])) { opt.attr[o] = ''; } tag.setAttribute(name, String(opt.attr[o])); } } } // Add the style for (var o in opt.style) { if (typeof o === 'string') { tag.style[o] = String(opt.style[o]); } } if (opt.parent) { opt.parent.appendChild(tag); } else { opt.svg.appendChild(tag); } return tag; }; // // Function that adds up all of the offsetLeft and offsetTops to get // X/Y coords for the mouse // //@param object e The event object //@return array The X/Y coordinate pair representing the mouse // location in relation to the SVG tag. // RG.SVG.getMouseXY = function(e) { // This is necessary for IE9 if (!e.target) { return; } var el = e.target, offsetX = 0, offsetY = 0, x, y; if (typeof el.offsetParent !== 'undefined') { do { offsetX += el.offsetLeft; offsetY += el.offsetTop; } while ((el = el.offsetParent)); } x = e.pageX; y = e.pageY; x -= (2 * (parseInt(document.body.style.borderLeftWidth) || 0)); y -= (2 * (parseInt(document.body.style.borderTopWidth) || 0)); // We return a javascript array with x and y defined return [x, y]; }; // // Draws an X axis // //@param The chart object // RG.SVG.drawXAxis = function (obj) { var prop = obj.properties; // Draw the axis if (prop.xaxis) { var y = obj.type === 'hbar' ? obj.height - prop.marginBottom : obj.getYCoord(obj.scale.min < 0 && obj.scale.max < 0 ? obj.scale.max : (obj.scale.min > 0 && obj.scale.max > 0 ? obj.scale.min : 0)); var axis = RG.SVG.create({ svg: obj.svg, parent: obj.svg.all, type: 'path', attr: { d: 'M{1} {2} L{3} {4}'.format( prop.marginLeft, y, obj.width - prop.marginRight, y ), fill: prop.xaxisColor, stroke: prop.xaxisColor, 'stroke-width': typeof prop.xaxisLinewidth === 'number' ? prop.xaxisLinewidth : 1, 'shape-rendering': 'crispEdges', 'stroke-linecap': 'square' } }); // HBar X axis if (obj.type === 'hbar') { var width = obj.graphWidth / obj.data.length, x = prop.marginLeft, startY = (obj.height - prop.marginBottom), endY = (obj.height - prop.marginBottom) + prop.xaxisTickmarksLength; // Line/Bar/Waterfall/Scatter X axis } else { var width = obj.graphWidth / obj.data.length, x = prop.marginLeft, startY = obj.getYCoord(0) - (prop.yaxisScaleMin < 0 ? prop.xaxisTickmarksLength : 0), endY = obj.getYCoord(0) + prop.xaxisTickmarksLength; if (obj.scale.min < 0 && obj.scale.max <= 0) { startY = prop.marginTop; endY = prop.marginTop - prop.xaxisTickmarksLength; } if (obj.scale.min > 0 && obj.scale.max > 0) { startY = obj.getYCoord(obj.scale.min); endY = obj.getYCoord(obj.scale.min) + prop.xaxisTickmarksLength; } } // Draw the tickmarks if (prop.xaxisTickmarks) { // The HBar uses a scale if (prop.xaxisScale) { for (var i=0; i<(typeof prop.xaxisLabelsPositionEdgeTickmarksCount === 'number' ? prop.xaxisLabelsPositionEdgeTickmarksCount : (obj.scale.numlabels + (prop.yaxis && prop.xaxisScaleMin === 0 ? 0 : 1))); ++i) { if (obj.type === 'hbar') { var dataPoints = obj.data.length; } x = prop.marginLeft + ((i+(prop.yaxis && prop.xaxisScaleMin === 0 ? 1 : 0)) * (obj.graphWidth / obj.scale.numlabels)); // Allow Manual specification of number of tickmarks if (typeof prop.xaxisLabelsPositionEdgeTickmarksCount === 'number') { dataPoints = prop.xaxisLabelsPositionEdgeTickmarksCount; var gap = (obj.graphWidth / prop.xaxisLabelsPositionEdgeTickmarksCount); x = (gap * i) + prop.marginLeft + gap; } RG.SVG.create({ svg: obj.svg, parent: obj.svg.all, type: 'path', attr: { d: 'M{1} {2} L{3} {4}'.format( x, startY, x, endY ), stroke: prop.xaxisColor, 'stroke-width': typeof prop.xaxisLinewidth === 'number' ? prop.xaxisLinewidth : 1, 'shape-rendering': "crispEdges" } }); } } else { // This style is used by Bar and Scatter charts if (prop.xaxisLabelsPosition === 'section') { if (obj.type === 'bar' || obj.type === 'waterfall') { var dataPoints = obj.data.length; } else if (obj.type === 'line'){ var dataPoints = obj.data[0].length; } else if (obj.type === 'scatter') { var dataPoints = prop.xaxisLabels ? prop.xaxisLabels.length : 10; } // Allow Manual specification of number of tickmarks if (typeof prop.xaxisLabelsPositionSectionTickmarksCount === 'number') { dataPoints = prop.xaxisLabelsPositionSectionTickmarksCount; } for (var i=0; i 0) { // WBB - line above changed to match the corresponding line in the RG.SVG.drawYAxis function if ((prop.xaxisScaleMin !== 0 || prop.yaxis === false || obj.mirrorScale) && ((obj.scale.min < 0 && obj.scale.max <= 0) || (obj.scale.min > 0 && obj.scale.max > 0))) { RG.SVG.create({ svg: obj.svg, parent: obj.svg.all, type: 'path', attr: { d: 'M{1} {2} L{3} {4}'.format( prop.marginLeft + (prop.marginInnerLeft || 0) + 0.001, startY, prop.marginLeft + (prop.marginInnerLeft || 0), endY ), stroke: obj.properties.xaxisColor, 'stroke-width': typeof prop.xaxisLinewidth === 'number' ? prop.xaxisLinewidth : 1, 'shape-rendering': "crispEdges", parent: obj.svg.all, } }); } } } // // Draw an X axis scale // if (prop.xaxisScale) { //if (obj.type === 'scatter') { // WBB - modified line above so that line graphs used the same X-axis as scatter graphs if (obj.type === 'scatter' || obj.type === 'line') { obj.xscale = RG.SVG.getScale({ object: obj, numlabels: prop.xaxisLabelsCount, unitsPre: prop.xaxisScaleUnitsPre, unitsPost: prop.xaxisScaleUnitsPost, max: prop.xaxisScaleMax, min: prop.xaxisScaleMin, point: prop.xaxisScalePoint, round: prop.xaxisScaleRound, thousand: prop.xaxisScaleThousand, decimals: prop.xaxisScaleDecimals, strict: typeof prop.xaxisScaleMax === 'number', formatter: prop.xaxisScaleFormatter }); var segment = obj.graphWidth / prop.xaxisLabelsCount for (var i=0; i 0) { var y = obj.height - prop.marginBottom + prop.xaxisLabelsOffsety + (prop.xaxis ? prop.xaxisTickmarksLength + 6 : 10), str = RG.SVG.numberFormat({ object: obj, num: prop.xaxisScaleMin.toFixed(prop.xaxisScaleDecimals), prepend: prop.xaxisScaleUnitsPre, append: prop.xaxisScaleUnitsPost, point: prop.xaxisScalePoint, thousand: prop.xaxisScaleThousand, formatter: prop.xaxisScaleFormatter }); var text = RG.SVG.text({ object: obj, parent: obj.svg.all, text: typeof prop.xaxisScaleFormatter === 'function' ? (prop.xaxisScaleFormatter)(this, prop.xaxisScaleMin) : str, x: prop.marginLeft + prop.xaxisLabelsOffsetx, y: y, halign: 'center', valign: 'top', tag: 'labels.xaxis', font: prop.xaxisLabelsFont || prop.textFont, size: typeof prop.xaxisLabelsSize === 'number' ? prop.xaxisLabelsSize : prop.textSize, bold: typeof prop.xaxisLabelsBold === 'number' ? prop.xaxisLabelsBold : prop.textBold, italic: typeof prop.xaxisLabelsItalic === 'boolean' ? prop.xaxisLabelsItalic : prop.textItalic, color: prop.xaxisLabelsColor || prop.textColor }); } // ========================================================================= } else { var segment = obj.graphWidth / prop.xaxisLabelsCount, scale = obj.scale; for (var i=0; i 0) { var y = obj.height - prop.marginBottom + prop.xaxisLabelsOffsety + (prop.xaxis ? prop.xaxisTickmarksLength + 6 : 10), str = RG.SVG.numberFormat({ object: obj, num: prop.xaxisScaleMin.toFixed(prop.xaxisScaleDecimals), prepend: prop.xaxisScaleUnitsPre, append: prop.xaxisScaleUnitsPost, point: prop.xaxisScalePoint, thousand: prop.xaxisScaleThousand, formatter: prop.xaxisScaleFormatter }); var text = RG.SVG.text({ object: obj, parent: obj.svg.all, text: typeof prop.xaxisScaleFormatter === 'function' ? (prop.xaxisScaleFormatter)(this, prop.xaxisScaleMin) : str, x: prop.marginLeft + prop.xaxisLabelsOffsetx, y: y, halign: 'center', valign: 'top', tag: 'labels.xaxis', font: prop.xaxisLabelsFont || prop.textFont, size: typeof prop.xaxisLabelsSize === 'number' ? prop.xaxisLabelsSize : prop.textSize, bold: typeof prop.xaxisLabelsBold === 'boolean' ? prop.xaxisLabelsBold : prop.textBold, italic: typeof prop.xaxisLabelsItalic === 'boolean' ? prop.xaxisLabelsItalic : prop.textItalic, color: prop.xaxisLabelsColor || prop.textColor }); } } // // Draw the X axis labels // } else { if (typeof prop.xaxisLabels === 'object' && !RG.SVG.isNull(prop.xaxisLabels) ) { var angle = prop.xaxisLabelsAngle; // Loop through the X labels if (prop.xaxisLabelsPosition === 'section') { var segment = (obj.width - prop.marginLeft - prop.marginRight - (prop.marginInnerLeft || 0) - (prop.marginInnerRight || 0) ) / prop.xaxisLabels.length; for (var i=0; i 0 ? prop.xaxisScaleMin : 0); if (prop.xaxisScaleMin < 0 && prop.xaxisScaleMax <= 0) { x = obj.getXCoord(prop.xaxisScaleMax); } // WBB this branch added to match the corresponding line in the RG.SVG.drawXAxis } else if (obj.type === 'scatter') { var x = obj.getXCoord(obj.scale.min < 0 && obj.scale.max < 0 ? obj.scale.max : (obj.scale.min > 0 && obj.scale.max > 0 ? obj.scale.min : 0)); } else { var x = prop.marginLeft; } var axis = RG.SVG.create({ svg: obj.svg, parent: obj.svg.all, type: 'path', attr: { d: 'M{1} {2} L{3} {4}'.format( x, prop.marginTop, x, obj.height - prop.marginBottom ), stroke: prop.yaxisColor, fill: prop.yaxisColor, 'stroke-width': typeof prop.yaxisLinewidth === 'number' ? prop.yaxisLinewidth : 1, 'shape-rendering': "crispEdges", 'stroke-linecap': 'square' } }); if (obj.type === 'hbar') { var height = (obj.graphHeight - prop.marginInnerTop - prop.marginInnerBottom) / prop.yaxisLabels.length, y = prop.marginTop + prop.marginInnerTop, len = prop.yaxisLabels.length, startX = obj.getXCoord(0) + (prop.xaxisScaleMin < 0 ? prop.yaxisTickmarksLength : 0), endX = obj.getXCoord(0) - prop.yaxisTickmarksLength; if (prop.xaxisScaleMin < 0 && prop.xaxisScaleMax <=0) { startX = obj.getXCoord(prop.xaxisScaleMax); endX = obj.getXCoord(prop.xaxisScaleMax) + 5; } // A custom number of tickmarks if (typeof prop.yaxisLabelsPositionSectionTickmarksCount === 'number') { len = prop.yaxisLabelsPositionSectionTickmarksCount; height = (obj.graphHeight - prop.marginInnerTop - prop.marginInnerBottom) / len; } // // Draw the tickmarks // if (prop.yaxisTickmarks) { for (var i=0; i 0 && obj.scale.max > 0)) { startX = prop.marginLeft; endX = prop.marginLeft - prop.yaxisTickmarksLength; } // A custom number of tickmarks if (typeof prop.yaxisLabelsPositionEdgeTickmarksCount === 'number') { len = prop.yaxisLabelsPositionEdgeTickmarksCount; height = obj.graphHeight / len; } // // Draw the tickmarks // if (prop.yaxisTickmarks) { for (var i=0; i 0 && obj.scale.max > 0) ) { // WBB lines above were changed to match the corresponding line in the RG.SVG.drawXAxis function if ((prop.yaxisScaleMin !== 0 || prop.xaxis === false || obj.mirrorScale) && (x === prop.marginLeft)) { var axis = RG.SVG.create({ svg: obj.svg, parent: obj.svg.all, type: 'path', attr: { d: 'M{1} {2} L{3} {4}'.format( prop.marginLeft - prop.yaxisTickmarksLength, obj.height - prop.marginBottom, prop.marginLeft, obj.height - prop.marginBottom - 0.001 ), stroke: prop.yaxisColor, 'stroke-width': typeof prop.yaxisLinewidth === 'number' ? prop.yaxisLinewidth : 1, 'shape-rendering': "crispEdges" } }); } } } } // // Draw the Y axis labels // if (prop.yaxisScale) { var segment = (obj.height - prop.marginTop - prop.marginBottom) / prop.yaxisLabelsCount; for (var i=0; i if (prop.backgroundImage) { var attr = { 'xlink:href': prop.backgroundImage, //preserveAspectRatio: 'xMidYMid slice', preserveAspectRatio: prop.backgroundImageAspect || 'none', x: prop.marginLeft, y: prop.marginTop }; if (prop.backgroundImageStretch) { attr.x = prop.marginLeft + prop.variant3dOffsetx; attr.y = prop.marginTop + prop.variant3dOffsety; attr.width = obj.width - prop.marginLeft - prop.marginRight; attr.height = obj.height - prop.marginTop - prop.marginBottom; } else { if (typeof prop.backgroundImageX === 'number') { attr.x = prop.backgroundImageX + prop.variant3dOffsetx; } else { attr.x = prop.marginLeft + prop.variant3dOffsetx; } if (typeof prop.backgroundImageY === 'number') { attr.y = prop.backgroundImageY + prop.variant3dOffsety; } else { attr.y = prop.marginTop + prop.variant3dOffsety; } if (typeof prop.backgroundImageW === 'number') { attr.width = prop.backgroundImageW; } if (typeof prop.backgroundImageH === 'number') { attr.height = prop.backgroundImageH; } } // // Account for the chart being 3d // if (prop.variant === '3d') { attr.x += prop.variant3dOffsetx; attr.y -= prop.variant3dOffsety; } var img = RG.SVG.create({ svg: obj.svg, parent: obj.svg.all, type: 'image', attr: attr, style: { opacity: typeof prop.backgroundImageOpacity === 'number' ? prop.backgroundImageOpacity : 1 } }); // Set the width and height if necessary if (!prop.backgroundImageStretch) { var img2 = new Image(); img2.src = prop.backgroundImage; img2.onload = function () { if (prop.backgroundImageW === 'number') img.setAttribute('width', prop.backgroundImageW); if (prop.backgroundImageH === 'number') img.setAttribute('height', prop.backgroundImageH); }; } } if (prop.backgroundGrid) { var parts = []; // Add the horizontal lines to the path if (prop.backgroundGridHlines) { if (typeof prop.backgroundGridHlinesCount === 'number') { var count = prop.backgroundGridHlinesCount; } else if (obj.type === 'hbar' || obj.type === 'bipolar') { if (typeof prop.yaxisLabels === 'object' && !RG.SVG.isNull(prop.yaxisLabels) && prop.yaxisLabels.length) { var count = prop.yaxisLabels.length; } else if (obj.type === 'hbar') { var count = obj.data.length; } else if (obj.type === 'bipolar') { var count = obj.left.length; } } else { var count = prop.yaxisLabelsCount || 5; } for (var i=0; i<=count; ++i) { parts.push('M{1} {2} L{3} {4}'.format( prop.marginLeft + prop.variant3dOffsetx, prop.marginTop + (obj.graphHeight / count) * i - prop.variant3dOffsety, obj.width - prop.marginRight + prop.variant3dOffsetx, prop.marginTop + (obj.graphHeight / count) * i - prop.variant3dOffsety )); } // Add an extra background grid line to the path - this its // underneath the X axis and shows up if it's not there. parts.push('M{1} {2} L{3} {4}'.format( prop.marginLeft + prop.variant3dOffsetx, obj.height - prop.marginBottom - prop.variant3dOffsety, obj.width - prop.marginRight + prop.variant3dOffsetx, obj.height - prop.marginBottom - prop.variant3dOffsety )); } // Add the vertical lines to the path if (prop.backgroundGridVlines) { if (obj.type === 'line' && RG.SVG.isArray(obj.data[0])) { var len = obj.data[0].length; } else if (obj.type === 'hbar') { var len = prop.xaxisLabelsCount || 10; } else if (obj.type === 'bipolar') { var len = prop.xaxisLabelsCount || 10; } else if (obj.type === 'scatter') { var len = (prop.xaxisLabels && prop.xaxisLabels.length) || 10; } else if (obj.type === 'waterfall') { var len = obj.data.length; } else { var len = obj.data.length; } var count = typeof prop.backgroundGridVlinesCount === 'number' ? prop.backgroundGridVlinesCount : len; if (prop.xaxisLabelsPosition === 'edge') { count--; } for (var i=0; i<=count; ++i) { parts.push('M{1} {2} L{3} {4}'.format( prop.marginLeft + ((obj.graphWidth / count) * i) + prop.variant3dOffsetx, prop.marginTop - prop.variant3dOffsety, prop.marginLeft + ((obj.graphWidth / count) * i) + prop.variant3dOffsetx, obj.height - prop.marginBottom - prop.variant3dOffsety )); } } // Add the box around the grid if (prop.backgroundGridBorder) { parts.push('M{1} {2} L{3} {4} L{5} {6} L{7} {8} z'.format( prop.marginLeft + prop.variant3dOffsetx, prop.marginTop - prop.variant3dOffsety, obj.width - prop.marginRight + prop.variant3dOffsetx, prop.marginTop - prop.variant3dOffsety, obj.width - prop.marginRight + prop.variant3dOffsetx, obj.height - prop.marginBottom - prop.variant3dOffsety, prop.marginLeft + prop.variant3dOffsetx, obj.height - prop.marginBottom - prop.variant3dOffsety )); } // Get the dash array if its defined to be dotted or dashed var dasharray; if (prop.backgroundGridDashed) { dasharray = [3,5]; } else if (prop.backgroundGridDotted) { dasharray = [1,3]; } else if (prop.backgroundGridDashArray) { dasharray = prop.backgroundGridDashArray; } else { dasharray = ''; } // Now draw the path var grid = RG.SVG.create({ svg: obj.svg, parent: obj.svg.all, type: 'path', attr: { className: 'rgraph_background_grid', d: parts.join(' '), stroke: prop.backgroundGridColor, fill: 'rgba(0,0,0,0)', 'stroke-width': prop.backgroundGridLinewidth, 'shape-rendering': "crispEdges", 'stroke-dasharray': dasharray }, style: { pointerEvents: 'none' } }); } // Draw the title and subtitle if (obj.type !== 'bipolar') { RG.SVG.drawTitle(obj); } }; /** * Returns true/false as to whether the given variable is null or not * * @param mixed arg The argument to check */ RG.SVG.isNull = function (arg) { // must BE DOUBLE EQUALS - NOT TRIPLE if (arg == null || typeof arg === 'object' && !arg) { return true; } return false; }; /** * Returns an appropriate scale. The return value is actualy an object consisting of: * scale.max * scale.min * scale.scale * * @param obj object The graph object * @param prop object An object consisting of configuration properties * @return object An object containg scale information */ RG.SVG.getScale = function (opt) { var obj = opt.object, prop = obj.properties, numlabels = opt.numlabels, unitsPre = opt.unitsPre, unitsPost = opt.unitsPost, max = Number(opt.max), min = Number(opt.min), strict = opt.strict, decimals = Number(opt.decimals), point = opt.point, thousand = opt.thousand, originalMax = max, round = opt.round, scale = {max:1,labels:[],values:[]}, formatter = opt.formatter; /** * Special case for 0 * * ** Must be first ** */ if (max === 0 && min === 0) { var max = 1; for (var i=0; i arr[i]) { i--; break; } } scale.max = arr[i] scale.labels = []; scale.values = []; for (var j=0; j Number(topValue)) { topValue += (interval / 2); } // Custom if the max is greater than 5 and less than 10 if (max <= 10) { topValue = (Number(originalMax) <= 5 ? 5 : 10); } // Added 02/11/2010 to create "nicer" scales if (obj && typeof(round) == 'boolean' && round) { topValue = 10 * interval; } scale.max = topValue; for (var i=0; i=0; i-=1) { newarr.push(arr[i]); } return newarr; }; /** * Makes a clone of an object * * @param obj val The object to clone */ RG.SVG.arrayClone = function (obj) { if(obj === null || typeof obj !== 'object') { return obj; } if (RG.SVG.isArray(obj)) { var temp = []; for (var i=0,len=obj.length;i 0 && pos < 20; }; /** * Returns the absolute value of a number. You can also pass in an * array and it will run the abs() function on each element. It * operates recursively so sub-arrays are also traversed. * * @param array arr The number or array to work on */ RG.SVG.abs = function (value) { if (typeof value === 'string') { value = parseFloat(value) || 0; } if (typeof value === 'number') { return ma.abs(value); } if (typeof value === 'object') { for (i in value) { if ( typeof i === 'string' || typeof i === 'number' || typeof i === 'object') { value[i] = RG.SVG.abs(value[i]); } } return value; } return 0; }; // // Formats a number with thousand seperators so it's easier to read // // @param opt object The options to the function // RG.SVG.numberFormat = function (opt) { var obj = opt.object, prepend = opt.prepend ? String(opt.prepend) : '', append = opt.append ? String(opt.append) : '', output = '', decimal_seperator = typeof opt.point === 'string' ? opt.point : '.', thousand_seperator = typeof opt.thousand === 'string' ? opt.thousand : ',', num = opt.num decimals_trim = opt.decimals_trim; RegExp.$1 = ''; if (typeof opt.formatter === 'function') { return opt.formatter(obj, num); } // Ignore the preformatted version of "1e-2" if (String(num).indexOf('e') > 0) { return String(prepend + String(num) + append); } // We need then number as a string num = String(num); // Take off the decimal part - we re-append it later if (num.indexOf('.') > 0) { var tmp = num; num = num.replace(/\.(.*)/, ''); // The front part of the number decimal = tmp.replace(/(.*)\.(.*)/, '$2'); // The decimal part of the number } else { decimal = ''; } // Thousand seperator //var seperator = arguments[1] ? String(arguments[1]) : ','; var seperator = thousand_seperator; /** * Work backwards adding the thousand seperators */ var foundPoint; for (i=(num.length - 1),j=0; i>=0; j++,i--) { var character = num.charAt(i); if ( j % 3 == 0 && j != 0) { output += seperator; } /** * Build the output */ output += character; } /** * Now need to reverse the string */ var rev = output; output = ''; for (i=(rev.length - 1); i>=0; i--) { output += rev.charAt(i); } // Tidy up //output = output.replace(/^-,/, '-'); if (output.indexOf('-' + thousand_seperator) == 0) { output = '-' + output.substr(('-' + thousand_seperator).length); } // Reappend the decimal if (decimal.length) { output = output + decimal_seperator + decimal; decimal = ''; RegExp.$1 = ''; } // // Trim the decimals if it's all zeros // if (decimals_trim) { output = output.replace(/0+$/,''); output = output.replace(/\.$/,''); } // Minor bugette if (output.charAt(0) == '-') { output = output.replace(/-/, ''); prepend = '-' + prepend; } return prepend + output + append; }; // // A function that adds text to the chart // RG.SVG.text = function (opt) { var obj = opt.object, parent = opt.parent || opt.object.svg.all, size = typeof opt.size === 'number' ? opt.size + 'pt' : (typeof opt.size === 'string' ? opt.size.replace(/pt$/,'') : 12) + 'pt', bold = opt.bold ? 'bold' : 'normal', font = opt.font ? opt.font : 'sans-serif', italic = opt.italic ? 'italic' : 'normal', halign = opt.halign, valign = opt.valign, str = opt.text, x = opt.x, y = opt.y, color = opt.color ? opt.color : 'black', background = opt.background || null, backgroundRounded = opt.backgroundRounded || 0, padding = opt.padding || 0, link = opt.link || '', linkTarget = opt.linkTarget || '_blank', events = (opt.events === false ? false : true), angle = opt.angle; // // Change numbers to strings // if (typeof str === 'number') { str = String(str); } // // Change null values to an empty string // if (RG.SVG.isNull(str)) { str = ''; } // // If the string starts with a carriage return add a unicode non-breaking // space to the start of it. // if (str && str.substr(0,2) == '\r\n' || str.substr(0,1) === '\n') { str = "\u00A0" + str; } // Horizontal alignment if (halign === 'right') { halign = 'end'; } else if (halign === 'center' || halign === 'middle') { halign = 'middle'; } else { halign = 'start'; } // Vertical alignment if (valign === 'top') { valign = 'hanging'; } else if (valign === 'center' || valign === 'middle') { valign = 'central'; valign = 'middle'; } else { valign = 'bottom'; } // // If a link has been specified then the text node should // be a child of an a node if (link) { var a = RGraph.SVG.create({ svg: obj.svg, type: 'a', parent: parent, attr: { 'xlink:href': link, target: linkTarget } }); } // // Text does not include carriage returns // if (str && str.indexOf && str.indexOf("\n") === -1) { var text = RG.SVG.create({ svg: obj.svg, parent: link ? a : opt.parent, type: 'text', attr: { tag: opt.tag ? opt.tag : '', fill: color, x: x, y: y, 'font-size': size, 'font-weight': bold, 'font-family': font, 'font-style': italic, 'text-anchor': halign, 'dominant-baseline': valign } }); var textNode = document.createTextNode(str); text.appendChild(textNode); if (!events) { text.style.pointerEvents = 'none'; } // // Includes carriage returns // } else if (str && str.indexOf) { // Measure the text var dimensions = RG.SVG.measureText({ text: 'My', bold: bold, font: font, size: size }); var lineHeight = dimensions[1]; str = str.split(/\r?\n/); // // Account for the carriage returns and move the text // up as required // if (valign === 'bottom') { y -= str.length * lineHeight; } if (valign === 'center' || valign === 'middle') { y -= (str.length * lineHeight) / 2; } var text = RG.SVG.create({ svg: obj.svg, parent: link ? a : opt.parent, type: 'text', attr: { tag: opt.tag ? opt.tag : '', fill: color, x: x, y: y, 'font-size': size, 'font-weight': bold, 'font-family': font, 'font-style': italic, 'text-anchor': halign, 'dominant-baseline': valign } }); if (!events) { text.style.pointerEvents = 'none'; } for (var i=0; i= 0) { if (RG.SVG.isNull(data[group])) { group++; grouped_index = 0; continue; } // Allow for numbers as well as arrays in the dataset if (typeof data[group] == 'number') { group++ grouped_index = 0; continue; } grouped_index++; if (grouped_index >= data[group].length) { group++; grouped_index = 0; } } return [group, grouped_index]; }; // // This is the reverse of the above function - converting // group/index to a sequential index // // @return number The sequential index // RG.SVG.groupedIndexToSequential = function (opt) { var dataset = opt.dataset, index = opt.index, obj = opt.object; for (var i=0,seq=0; i<=dataset; ++i) { for (var j=0; j= cx && y >= cy) { angle += RG.SVG.TRIG.HALFPI; } else if (x >= cx && y < cy) { angle = angle + RG.SVG.TRIG.HALFPI; } else if (x < cx && y < cy) { angle = angle + RG.SVG.TRIG.PI + RG.SVG.TRIG.HALFPI; } else { angle = angle + RG.SVG.TRIG.PI + RG.SVG.TRIG.HALFPI; } return angle; }; // // Gets a path that is usable by the SVG A path command // // @patam object options The options/arg to the function // // NB ** Still used by the Pie chart and the semi-circular Meter ** // RG.SVG.TRIG.getArcPath = function (options) { // // Make circles start at the top instead of the right hand side // options.start -= 1.57; options.end -= 1.57; var start = RG.SVG.TRIG.toCartesian({ cx: options.cx, cy: options.cy, r: options.r, angle: options.start} ); var end = RG.SVG.TRIG.toCartesian({ cx: options.cx, cy: options.cy, r: options.r, angle: options.end }); var diff = options.end - options.start; // Initial values var largeArc = '0'; var sweep = '0'; if (options.anticlockwise && diff > 3.14) { largeArc = '0'; sweep = '0'; } else if (options.anticlockwise && diff <= 3.14) { largeArc = '1'; sweep = '0'; } else if (!options.anticlockwise && diff > 3.14) { largeArc = '1'; sweep = '1'; } else if (!options.anticlockwise && diff <= 3.14) { largeArc = '0'; sweep = '1'; } if (options.start > options.end && options.anticlockwise && diff <= 3.14) { largeArc = '0'; sweep = '0'; } if (options.start > options.end && options.anticlockwise && diff > 3.14) { largeArc = '1'; sweep = '1'; } if (typeof options.moveto === 'boolean' && options.moveto === false) { var d = [ "A", options.r, options.r, 0, largeArc, sweep, end.x, end.y ]; } else { var d = [ "M", start.x, start.y, "A", options.r, options.r, 0, largeArc, sweep, end.x, end.y ]; } if (options.array === true) { return d; } else { return d.join(" "); } }; // // Gets a path that is usable by the SVG A path command // // @patam object options The options/arg to the function // RG.SVG.TRIG.getArcPath2 = function (options) { // // Make circles start at the top instead of the right hand side // options.start -= 1.57; options.end -= 1.57; var start = RG.SVG.TRIG.toCartesian({ cx: options.cx, cy: options.cy, r: options.r, angle: options.start }); var end = RG.SVG.TRIG.toCartesian({ cx: options.cx, cy: options.cy, r: options.r, angle: options.end }); var diff = ma.abs(options.end - options.start); // Initial values var largeArc = '0'; var sweep = '0'; //TODO Put various options here for the correct combination of flags to use if (!options.anticlockwise) { if (diff > RG.SVG.TRIG.PI) { largeArc = '1'; sweep = '1'; } else { largeArc = '0'; sweep = '1'; } } else { if (diff > RG.SVG.TRIG.PI) { largeArc = '1'; sweep = '0'; } else { largeArc = '0'; sweep = '0'; } } if (typeof options.lineto === 'boolean' && options.lineto === false) { var d = [ "M", start.x, start.y, "A", options.r, options.r, 0, largeArc, sweep, end.x, end.y ]; } else { var d = [ "M", options.cx, options.cy, "L", start.x, start.y, "A", options.r, options.r, 0, largeArc, sweep, end.x, end.y ]; } if (options.array === true) { return d; } else { return d.join(" "); } }; // // Gets a path that is usable by the SVG A path command // // @param object options The options/arg to the function // RG.SVG.TRIG.getArcPath3 = function (options) { // Make sure the args are numbers options.cx = Number(options.cx); options.cy = Number(options.cy); options.radius = Number(options.radius); options.start = Number(options.start); options.end = Number(options.end); // // Make circles start at the top instead of the right hand side // options.start -= (ma.PI / 2); options.end -= (ma.PI / 2); var start = RG.SVG.TRIG.toCartesian({ cx: options.cx, cy: options.cy, r: options.r, angle: options.start }); var end = RG.SVG.TRIG.toCartesian({ cx: options.cx, cy: options.cy, r: options.r, angle: options.end }); var diff = ma.abs(options.end - options.start); // Initial values var largeArc = '0'; var sweep = '0'; //TODO Put various options here for the correct combination of flags to use if (!options.anticlockwise) { if (diff > RG.SVG.TRIG.PI) { largeArc = '1'; sweep = '1'; } else { largeArc = '0'; sweep = '1'; } } else { if (diff > RG.SVG.TRIG.PI) { largeArc = '1'; sweep = '0'; } else { largeArc = '0'; sweep = '0'; } } if (typeof options.lineto === 'boolean' && options.lineto === false) { var d = [ "M", start.x, start.y, "A", options.r, options.r, 0, largeArc, sweep, end.x, end.y ]; } else { var d = [ "L", start.x, start.y, "A", options.r, options.r, 0, largeArc, sweep, end.x, end.y ]; } if (options.array === true) { return d; } else { return d.join(" "); } }; /** * This function gets the end point (X/Y coordinates) of a given radius. * You pass it the center X/Y and the radius and this function will return * the endpoint X/Y coordinates. * * @param number cx The center X coord * @param number cy The center Y coord * @param number r The length of the radius * @param number angle The anle to use */ RG.SVG.TRIG.getRadiusEndPoint = function (opt) { // Allow for two arguments style if (arguments.length === 1) { var angle = opt.angle, r = opt.r; } else if (arguments.length === 4) { var angle = arguments[0], r = arguments[1]; } var x = ma.cos(angle) * r, y = ma.sin(angle) * r; return [x, y]; }; /** * This function draws the title. This function also draws the subtitle. */ RG.SVG.drawTitle = function (obj) { var prop = obj.properties; // Work out the X cooordinate for the title and subtitle var x = ((obj.width - prop.marginLeft - prop.marginRight) / 2) + prop.marginLeft, y = prop.marginTop - 10, valign = 'bottom'; // If theres key defined then move the title up if (!RG.SVG.isNull(obj.properties.key)) { y -= 20; } // If X axis is at the top then move the title up if ( typeof obj.properties.yaxisScaleMax === 'number' && obj.properties.yaxisScaleMax <= 0 && obj.properties.yaxisScaleMin < obj.properties.yaxisScaleMax ) { valign = 'top'; y = obj.height - obj.properties.marginBottom + 10; } // Custom X coordinate if (typeof prop.titleX === 'number') { x = prop.titleX; } // Custom Y coordinate if (typeof prop.titleY === 'number') { y = prop.titleY; } // Custom X coordinate if (typeof prop.titleOffsetx === 'number') { x += prop.titleOffsetx; } // Custom Y coordinate if (typeof prop.titleOffsety === 'number') { y += prop.titleOffsety; } // Move the Y cood up if thers a subtitle if (typeof prop.titleSubtitle === 'string' || typeof prop.titleSubtitle === 'number') { var titleSubtitleDim = RG.SVG.measureText({ bold: prop.titleSubtitleBold, italic: prop.titleSubtitleItalic, size: prop.titleSubtitleSize, font: prop.titleSubtitleFont, text: 'Mg' }); y -= titleSubtitleDim[1]; } // Draw the title if (prop.title) { RG.SVG.text({ object: obj, svg: obj.svg, parent: obj.svg.all, tag: 'title', text: prop.title.toString(), x: x, y: y, halign: prop.titleHalign || 'center', valign: valign, color: prop.titleColor || prop.textColor, size: typeof prop.titleSize === 'number' ? prop.titleSize : prop.textSize + 4, bold: typeof prop.titleBold === 'boolean' ? prop.titleBold : prop.textBold, italic: typeof prop.titleItalic === 'boolean' ? prop.titleItalic : prop.textItalic, font: prop.titleFont || prop.textFont }); } // Draw the subtitle if there's one defined if ( (typeof prop.title === 'string' || typeof prop.title === 'number') && (typeof prop.titleSubtitle === 'string' || typeof prop.titleSubtitle === 'number') ) { RG.SVG.text({ object: obj, svg: obj.svg, parent: obj.svg.all, tag: 'subtitle', text: prop.titleSubtitle, x: x, y: y + 5, halign: prop.titleHalign || 'center', valign: 'top', size: typeof prop.titleSubtitleSize === 'number' ? prop.titleSubtitleSize : prop.textSize - 2, color: prop.titleSubtitleColor || prop.textColor, bold: typeof prop.titleSubtitleBold === 'boolean' ? prop.titleSubtitleBold : prop.textBold, italic: typeof prop.titleSubtitleItalic === 'boolean' ? prop.titleSubtitleItalic : prop.textItalic, font: prop.titleSubtitleFont || prop.textFont }); } }; /** * Removes white-space from the start and end of a string * * @param string str The string to trim */ RG.SVG.trim = function (str) { return RG.SVG.ltrim(RG.SVG.rtrim(str)); }; /** * Trims the white-space from the start of a string * * @param string str The string to trim */ RG.SVG.ltrim = function (str) { return String(str).replace(/^(\s|\0)+/, ''); }; /** * Trims the white-space off of the end of a string * * @param string str The string to trim */ RG.SVG.rtrim = function (str) { return String(str).replace(/(\s|\0)+$/, ''); }; /** * This parses a single color value */ RG.SVG.parseColorLinear = function (opt) { var obj = opt.object, color = opt.color; if (!color || typeof color !== 'string') { return color; } if (color.match(/^gradient\((.*)\)$/i)) { var parts = RegExp.$1.split(':'), diff = 1 / (parts.length - 1); if (opt && opt.direction && opt.direction === 'horizontal') { var grad = RG.SVG.create({ type: 'linearGradient', parent: obj.svg.defs, attr: { id: 'RGraph-linear-gradient-' + obj.uid + '-' + obj.gradientCounter, x1: opt.start || 0, x2: opt.end || '100%', y1: 0, y2: 0, gradientUnits: opt.gradientUnits || "userSpaceOnUse" } }); } else { var grad = RG.SVG.create({ type: 'linearGradient', parent: obj.svg.defs, attr: { id: 'RGraph-linear-gradient-' + obj.uid + '-' + obj.gradientCounter, x1: 0, x2: 0, y1: opt.start || 0, y2: opt.end || '100%', gradientUnits: opt.gradientUnits || "userSpaceOnUse" } }); } // Add the first color stop var stop = RG.SVG.create({ type: 'stop', parent: grad, attr: { offset: '0%', 'stop-color': RG.SVG.trim(parts[0]) } }); // Add the rest of the color stops for (var j=1,len=parts.length; j 0) { for(var j=0,len=RG.SVG.events[uid].length; j 0) { x += parseInt(document.body.style.borderLeftWidth) || 0; y += parseInt(document.body.style.borderTopWidth) || 0; } return [x + paddingLeft + borderLeft, y + paddingTop + borderTop]; }; // // This function is a compatibility wrapper around // the requestAnimationFrame function. // // @param function func The function to give to the // requestAnimationFrame function // RG.SVG.FX.update = function (func) { win.requestAnimationFrame = win.requestAnimationFrame || win.webkitRequestAnimationFrame || win.msRequestAnimationFrame || win.mozRequestAnimationFrame || (function (func){setTimeout(func, 16.666);}); win.requestAnimationFrame(func); }; /** * This function returns an easing multiplier for effects so they eas out towards the * end of the effect. * * @param number frames The total number of frames * @param number frame The frame number */ RG.SVG.FX.getEasingMultiplier = function (frames, frame) { var multiplier = ma.pow(ma.sin((frame / frames) * RG.SVG.TRIG.HALFPI), 3); return multiplier; }; /** * Measures text by creating a DIV in the document and adding the relevant * text to it, then checking the .offsetWidth and .offsetHeight. * * @param object opt An object containing the following: * o text( string) The text to measure * o bold (bool) Whether the text is bold or not * o font (string) The font to use * o size (number) The size of the text (in pts) * * @return array A two element array of the width and height of the text */ RG.SVG.measureText = function (opt) { //text, bold, font, size var text = opt.text || '', bold = opt.bold || false, italic = opt.italic || false, font = opt.font || 'sans-serif', size = opt.size || 12, str = text + ':' + italic + ':' + bold + ':' + font + ':' + size; // Add the sizes to the cache as adding DOM elements is costly and causes slow downs if (typeof RG.SVG.measuretext_cache === 'undefined') { RG.SVG.measuretext_cache = []; } if (typeof RG.SVG.measuretext_cache == 'object' && RG.SVG.measuretext_cache[str]) { return RG.SVG.measuretext_cache[str]; } if (!RG.SVG.measuretext_cache['text-span']) { var span = document.createElement('SPAN'); span.style.position = 'absolute'; span.style.padding = 0; span.style.display = 'inline'; span.style.top = '-200px'; span.style.left = '-200px'; span.style.lineHeight = '1em'; document.body.appendChild(span); // Now store the newly created DIV RG.SVG.measuretext_cache['text-span'] = span; } else if (RG.SVG.measuretext_cache['text-span']) { var span = RG.SVG.measuretext_cache['text-span']; } span.innerHTML = text.replace(/\r?\n/g, '
'); span.style.fontFamily = font; span.style.fontWeight = bold ? 'bold' : 'normal'; span.style.fontStyle = italic ? 'italic' : 'normal'; span.style.fontSize = String(size).replace(/pt$/, '') + 'pt'; var sizes = [span.offsetWidth, span.offsetHeight]; //document.body.removeChild(span); RG.SVG.measuretext_cache[str] = sizes; return sizes; }; /** * This function converts an array of strings to an array of numbers. Its used by the meter/gauge * style charts so that if you want you can pass in a string. It supports various formats: * * '45.2' * '-45.2' * ['45.2'] * ['-45.2'] * '45.2,45.2,45.2' // A CSV style string * * @param number frames The string or array to parse */ RG.SVG.stringsToNumbers = function (str) { // An optional seperator to use intead of a comma var sep = arguments[1] || ','; // If it's already a number just return it if (typeof str === 'number') { return str; } if (typeof str === 'string') { if (str.indexOf(sep) != -1) { str = str.split(sep); } else { str = parseFloat(str); } } if (typeof str === 'object') { for (var i=0,len=str.length; i= 5) { return ''; } switch (typeof obj) { case 'string': str += obj + ' (' + (typeof obj) + ', ' + obj.length + ')'; break; case 'number': str += obj + ' (' + (typeof obj) + ')'; break; case 'boolean': str += obj + ' (' + (typeof obj) + ')'; break; case 'function': str += 'function () {}'; break; case 'undefined': str += 'undefined'; break; case 'null': str += 'null'; break; case 'object': // In case of null if (RGraph.SVG.isNull(obj)) { str += indent + 'null\n'; } else { str += indent + 'Object {' + '\n' for (j in obj) { str += indent + ' ' + j + ' => ' + window.$p(obj[j], true, indent + ' ', counter + 1) + '\n'; } str += indent + '}'; } break; default: str += 'Unknown type: ' + typeof obj + ''; break; } /** * Finished, now either return if we're in a recursed call, or alert() * if we're not. */ if (!arguments[1]) { alert(str); } return str; }; /** * A shorthand for the default alert() function */ window.$a = function (v) { alert(v); }; /** * Short-hand for console.log * * @param mixed v The variable to log to the console */ window.$cl = function (v) { return console.log(v); }; /** * A basic string formatting function. Use it like this: * * var str = '{1} {2} {3}'.format('a', 'b', 'c'); * * Outputs: a b c */ if (!String.prototype.format) { String.prototype.format = function() { var args = arguments; return this.replace(/{(\d+)}/g, function(str, idx) { return typeof args[idx - 1] !== 'undefined' ? args[idx - 1] : str; }); }; }