// 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) { RGraph.SVG.Line = function (conf) { // // A setter that the constructor uses (at the end) // to set all of the properties // // @param string name The name of the property to set // @param string value The value to set the property to // this.set = function (name, value) { if (arguments.length === 1 && typeof name === 'object') { for (i in arguments[0]) { if (typeof i === 'string') { this.set(i, arguments[0][i]); } } } else { // Go through all of the properties and make sure // that they're using the correct capitalisation name = this.properties_lowercase_map[name.toLowerCase()] || name; var ret = RGraph.SVG.commonSetter({ object: this, name: name, value: value }); name = ret.name; value = ret.value; this.properties[name] = value; // If setting the colors, update the originalColors // property too if (name === 'colors') { this.originalColors = RGraph.SVG.arrayClone(value, true); this.colorsParsed = false; } } return this; }; // // A getter. // // @param name string The name of the property to get // this.get = function (name) { // Go through all of the properties and make sure // that they're using the correct capitalisation name = this.properties_lowercase_map[name.toLowerCase()] || name; return this.properties[name]; }; this.type = 'line'; this.id = conf.id; this.uid = RGraph.SVG.createUID(); this.container = document.getElementById(this.id); this.layers = {}; // MUST be before the SVG tag is created! this.svg = RGraph.SVG.createSVG({ object: this, container: this.container }); this.svgAllGroup = RGraph.SVG.createAllGroup(this); this.isRGraph = true; this.isrgraph = true; this.rgraph = true; this.width = Number(this.svg.getAttribute('width')); this.height = Number(this.svg.getAttribute('height')); this.firstDraw = true; // After the first draw this will be false this.clipid = null; // Used to clip the canvas // Convert strings to numbers conf.data = RGraph.SVG.stringsToNumbers(conf.data); // Convert single datasets to a multi-dimensional format if (RGraph.SVG.isArray(conf.data) && RGraph.SVG.isArray(conf.data[0])) { this.data = RGraph.SVG.arrayClone(conf.data, true); } else if (RGraph.SVG.isArray(conf.data)) { this.data = [RGraph.SVG.arrayClone(conf.data, true)]; } else { this.data = [[]]; } this.coords = []; this.coords2 = []; this.coordsSpline = []; this.coordsTrendline = []; this.hasMultipleDatasets = typeof this.data[0] === 'object' && typeof this.data[1] === 'object' ? true : false; this.colorsParsed = false; this.originalColors = {}; this.gradientCounter = 1; this.originalData = RGraph.SVG.arrayClone(this.data, true); this.filledGroups = []; // Add this object to the ObjectRegistry RGraph.SVG.OR.add(this); this.container.style.display = 'inline-block'; this.properties = { marginLeft: 35, marginRight: 35, marginTop: 35, marginBottom: 35, marginInner: 0, backgroundColor: null, backgroundImage: null, backgroundImageStretch: true, backgroundImageAspect: 'none', backgroundImageOpacity: null, backgroundImageX: null, backgroundImageY: null, backgroundImageW: null, backgroundImageH: null, backgroundGrid: true, backgroundGridColor: '#ddd', backgroundGridLinewidth: 1, backgroundGridHlines: true, backgroundGridHlinesCount: null, backgroundGridVlines: true, backgroundGridVlinesCount: null, backgroundGridBorder: true, backgroundGridDashed: false, backgroundGridDotted: false, backgroundGridDashArray: null, colors: ['red', '#0f0', 'blue', '#ff0', '#0ff', 'green'], filled: false, filledDualColor: false, filledColors: [], filledClick: null, filledOpacity: 1, filledAccumulative: false, yaxis: true, yaxisLinewidth: 1, yaxisTickmarks: true, yaxisTickmarksLength: 3, yaxisColor: 'black', yaxisScale: true, yaxisLabels: null, yaxisLabelsOffsetx: 0, yaxisLabelsOffsety: 0, yaxisLabelsCount: 5, yaxisScaleUnitsPre: '', yaxisScaleUnitsPost: '', yaxisScaleStrict: false, yaxisScaleDecimals: 0, yaxisScalePoint: '.', yaxisScaleThousand: ',', yaxisScaleRound: false, yaxisScaleMax: null, yaxisScaleMin: 0, yaxisScaleFormatter: null, yaxisLabelsFont: null, yaxisLabelsSize: null, yaxisLabelsColor: null, yaxisLabelsBold: null, yaxisLabelsItalic: null, yaxisTitle: '', yaxisTitleBold: null, yaxisTitleSize: null, yaxisTitleFont: null, yaxisTitleColor: null, yaxisTitleItalic: null, yaxisTitleOffsetx: 0, yaxisTitleOffsety: 0, yaxisTitleX: null, yaxisTitleY: null, yaxisTitleHalign: null, yaxisTitleValign: null, xaxis: true, xaxisLinewidth: 1, xaxisTickmarks: true, xaxisTickmarksLength: 5, xaxisLabels: null, xaxisLabelsOffsetx: 0, xaxisLabelsOffsety: 0, xaxisLabelsPosition: 'edge', xaxisLabelsPositionEdgeTickmarksCount: null, xaxisColor: 'black', xaxisLabelsFont: null, xaxisLabelsSize: null, xaxisLabelsColor: null, xaxisLabelsBold: null, xaxisLabelsItalic: null, xaxisLabelsFormattedDecimals: 0, xaxisLabelsFormattedPoint: '.', xaxisLabelsFormattedThousand: ',', xaxisLabelsFormattedUnitsPre: '', xaxisLabelsFormattedUnitsPost: '', xaxisTitle: '', xaxisTitleBold: null, xaxisTitleSize: null, xaxisTitleFont: null, xaxisTitleColor: null, xaxisTitleItalic: null, xaxisTitleOffsetx: 0, xaxisTitleOffsety: 0, xaxisTitleX: null, xaxisTitleY: null, xaxisTitleHalign: null, xaxisTitleValign: null, textColor: 'black', textFont: 'Arial, Verdana, sans-serif', textSize: 12, textBold: false, textItalic: false, text: null, linewidth: 1, linejoin: 'round', linecap: 'round', tooltips: null, tooltipsOverride: null, tooltipsEffect: 'fade', tooltipsCssClass: 'RGraph_tooltip', tooltipsCss: null, tooltipsEvent: 'mousemove', tooltipsPersistent: false, tooltipsFormattedThousand: ',', tooltipsFormattedPoint: '.', tooltipsFormattedDecimals: 0, tooltipsFormattedUnitsPre: '', tooltipsFormattedUnitsPost: '', tooltipsFormattedKeyColors: null, tooltipsFormattedKeyColorsShape: 'square', tooltipsFormattedKeyLabels: [], tooltipsFormattedTableHeaders: null, tooltipsFormattedTableData: null, tooltipsPointer: true, tooltipsPointerOffsetx: 0, tooltipsPointerOffsety: 0, tooltipsPositionStatic: true, tooltipsDataset: null, tooltipsHotspotSize: 5, //highlightStroke: 'rgba(0,0,0,0)', //highlightFill: 'rgba(255,255,255,0.7)', //highlightLinewidth: 1, tickmarksStyle: 'none', tickmarksSize: 5, tickmarksFill: 'white', tickmarksLinewidth: 1, labelsAbove: false, labelsAboveFont: null, labelsAboveSize: null, labelsAboveBold: null, labelsAboveItalic: null, labelsAboveColor: null, labelsAboveBackground: 'rgba(255,255,255,0.7)', labelsAboveBackgroundPadding: 2, labelsAboveUnitsPre: null, labelsAboveUnitsPost: null, labelsAbovePoint: null, labelsAboveThousand: null, labelsAboveFormatter: null, labelsAboveDecimals: null, labelsAboveOffsetx: 0, labelsAboveOffsety: -10, labelsAboveHalign: 'center', labelsAboveValign: 'bottom', labelsAboveSpecific: null, shadow: false, shadowOffsetx: 2, shadowOffsety: 2, shadowBlur: 2, shadowColor: 'rgba(0,0,0,0.25)', spline: false, stepped: false, title: '', titleX: null, titleY: null, titleHalign: 'center', titleValign: null, titleSize: null, titleColor: null, titleFont: null, titleBold: null, titleItalic: null, titleSubtitle: null, titleSubtitleSize: null, titleSubtitleColor: '#aaa', titleSubtitleFont: null, titleSubtitleBold: null, titleSubtitleItalic: null, errorbars: null, errorbarsColor: 'black', errorbarsLinewidth: 1, errorbarsCapwidth: 10, key: null, keyColors: null, keyOffsetx: 0, keyOffsety: 0, keyLabelsOffsetx: 0, keyLabelsOffsety: -1, keyLabelsSize: null, keyLabelsBold: null, keyLabelsItalic: null, keyLabelsFont: null, keyLabelsColor: null, dasharray: null, dashed: false, dotted: false, highlightFill: null, trendline: false, trendlineColors: ['#666'], trendlineLinewidth: 1, trendlineMargin: 25, trendlineDashed: false, trendlineDotted: false, trendlineDashArray: null, trendlineClip: true, nullBridge: false, nullBridgeLinewidth: null, nullBridgeColors: null, // Can be null, a string or an object nullBridgeDashArray: null, clip: null, zoom: false }; // // Add the reverse look-up table for property names // so that property names can be specified in any case. // this.properties_lowercase_map = []; for (var i in this.properties) { if (typeof i === 'string') { this.properties_lowercase_map[i.toLowerCase()] = i; } } // // Copy the global object properties to this instance // RGraph.SVG.getGlobals(this); // // "Decorate" the object with the generic effects if the effects library has been included // if (RGraph.SVG.FX && typeof RGraph.SVG.FX.decorate === 'function') { RGraph.SVG.FX.decorate(this); } // Add the responsive function to the object this.responsive = RGraph.SVG.responsive; var properties = this.properties; // // The draw method draws the Bar chart // this.draw = function () { // Fire the beforedraw event RGraph.SVG.fireCustomEvent(this, 'onbeforedraw'); // Should the first thing that's done in the.draw() function // except for the onbeforedraw event and the // installation of clipping. this.width = Number(this.svg.getAttribute('width')); this.height = Number(this.svg.getAttribute('height')); // Create the defs tag RGraph.SVG.createDefs(this); this.graphWidth = this.width - properties.marginLeft - properties.marginRight; this.graphHeight = this.height - properties.marginTop - properties.marginBottom; // Parse the colors for gradients RGraph.SVG.resetColorsToOriginalValues({object:this}); this.parseColors(); // Clear the coords arrays this.coords = []; this.coords2 = []; this.coordsSpline = []; // Reset the data back to the original this.data = RGraph.SVG.arrayClone(this.originalData, true); // Set this to zero this.tooltipsSequentialIndex = 0; // If the line is set to be dotted or dashed then set the dash array if (properties.dashed) { properties.dasharray = [5,5]; } if (properties.dotted) { properties.dasharray = [1,4]; } // Make the data sequential first this.data_seq = RGraph.SVG.arrayLinearize(this.data); // This allows the errorbars to be a variety of formats and convert // them all into an array of objects which have the min and max // properties set if (properties.errorbars) { // Go through the error bars and convert numbers to objects for (var i=0; i<this.data_seq.length; ++i) { if (typeof properties.errorbars[i] === 'undefined' || RGraph.SVG.isNullish(properties.errorbars[i]) ) { properties.errorbars[i] = {max: null, min: null}; } else if (typeof properties.errorbars[i] === 'number') { properties.errorbars[i] = { min: properties.errorbars[i], max: properties.errorbars[i] }; // Max is undefined } else if (typeof properties.errorbars[i] === 'object' && typeof properties.errorbars[i].max === 'undefined') { properties.errorbars[i].max = null; // Min is not defined } else if (typeof properties.errorbars[i] === 'object' && typeof properties.errorbars[i].min === 'undefined') { properties.errorbars[i].min = null; } } } // Go through all the data working out the max value // whilst taking errorbars into account for (var i=0,tmp=[]; i<this.data.length; ++i) { for (var j=0; j<this.data[i].length; ++j) { // Init the tmp array slot if (typeof tmp[j] === 'undefined') { tmp[j] = 0; } if (properties.filled && properties.filledAccumulative) { tmp[j] += this.data[i][j]; // Only add this once (from the last dataset) if (i === (this.data.length - 1) ) { tmp[j] += (properties.errorbars ? properties.errorbars[RGraph.SVG.groupedIndexToSequential({object: this, dataset: i, index: j})].max : 0) } } else { tmp[j] = Math.max( tmp[j], this.data[i][j] + (properties.errorbars ? properties.errorbars[RGraph.SVG.groupedIndexToSequential({object: this, dataset: i, index: j})].max : 0) ); } } } // Go through the data and work out the maximum value var values = []; // Go thru each dataset for (var i=0,max=0; i<this.data.length; ++i) { if (RGraph.SVG.isArray(this.data[i]) && !properties.filledAccumulative) { values.push(RGraph.SVG.arrayMax(tmp)); } else if (RGraph.SVG.isArray(this.data[i]) && properties.filled && properties.filledAccumulative) { for (var j=0; j<this.data[i].length; ++j) { values[j] = values[j] || 0; values[j] = values[j] + this.data[i][j]; // This adds values to prior values in order // to create the stacking effect. this.data[i][j] = values[j]; } } } if (properties.filled && properties.filledAccumulative) { var max = RGraph.SVG.arrayMax(tmp) } else { var max = RGraph.SVG.arrayMax(values, true); } // A custom, user-specified maximum value if (typeof properties.yaxisScaleMax === 'number') { max = properties.yaxisScaleMax; } // Set the ymin to zero if it's set mirror if (properties.yaxisScaleMin === 'mirror') { this.mirrorScale = true; properties.yaxisScaleMin = 0; } // // Generate an appropiate scale // this.scale = RGraph.SVG.getScale({ object: this, numlabels: properties.yaxisLabelsCount, unitsPre: properties.yaxisScaleUnitsPre, unitsPost: properties.yaxisScaleUnitsPost, max: max, min: properties.yaxisScaleMin, point: properties.yaxisScalePoint, round: properties.yaxisScaleRound, thousand: properties.yaxisScaleThousand, decimals: properties.yaxisScaleDecimals, strict: typeof properties.yaxisScaleMax === 'number', formatter: properties.yaxisScaleFormatter }); // // Get the scale a second time if the ymin should be mirored // // Set the ymin to zero if it's szet mirror if (this.mirrorScale) { this.scale = RGraph.SVG.getScale({ object: this, numlabels: properties.yaxisLabelsCount, unitsPre: properties.yaxisScaleUnitsPre, unitsPost: properties.yaxisScaleUnitsPost, max: this.scale.max, min: this.scale.max * -1, point: properties.yaxisScalePoint, round: false, thousand: properties.yaxisScaleThousand, decimals: properties.yaxisScaleDecimals, strict: true, formatter: properties.yaxisScaleFormatter }); } // Now the scale has been generated adopt its max value this.max = this.scale.max; this.min = this.scale.min; // Taken out 14/01/18 so that the scale is not fixed // across draws // //properties.yaxisScaleMax = this.scale.max; //properties.yaxisScaleMin = this.scale.min; // Install clipping if requested if (this.properties.clip) { this.clipid = RGraph.SVG.installClipping(this); // Add the clip ID to the all group this.svgAllGroup.setAttribute( 'clip-path', 'url(#{1})'.format(this.clipid) ); } else { // No clipping - so ensure that there's no clip-path // attribute this.clipid = null; this.svgAllGroup.removeAttribute('clip-path'); } // Draw the background first RGraph.SVG.drawBackground(this); // // If the xaxisLabels option is a string then turn it // into an array. // if (properties.xaxisLabels && properties.xaxisLabels.length) { if (typeof properties.xaxisLabels === 'string') { properties.xaxisLabels = RGraph.SVG.arrayPad( [], this.originalData[0].length, properties.xaxisLabels ); } // Label substitution // for (var i=0; i<properties.xaxisLabels.length; ++i) { properties.xaxisLabels[i] = RGraph.SVG.labelSubstitution({ object: this, text: properties.xaxisLabels[i], index: i, value: this.originalData[0][i], decimals: properties.xaxisLabelsFormattedDecimals || 0, unitsPre: properties.xaxisLabelsFormattedUnitsPre || '', unitsPost: properties.xaxisLabelsFormattedUnitsPost || '', thousand: properties.xaxisLabelsFormattedThousand || ',', point: properties.xaxisLabelsFormattedPoint || '.' }); } } // Draw the axes over the bars RGraph.SVG.drawXAxis(this); RGraph.SVG.drawYAxis(this); for (var i=0; i<this.data.length; ++i) { this.drawLine(this.data[i], i); } // Always redraw the lines now so that tickmarks // are drawn this.redrawLines(); // Add trendlines if they have been enabled for (let i=0; i<this.data.length; ++i) { if ( (RGraph.SVG.isArray(properties.trendline) && properties.trendline[i]) || (!RGraph.SVG.isArray(properties.trendline) && properties.trendline)) { this.drawTrendline({dataset: i}); } } // // Bridge the null gaps if requested // if (properties.nullBridge) { for (var i=0; i<this.data.length; ++i) { this.nullBridge(i, this.data[i]); } } // Draw the key if (typeof properties.key !== null && RGraph.SVG.drawKey) { RGraph.SVG.drawKey(this); } else if (!RGraph.SVG.isNullish(properties.key)) { alert('The drawKey() function does not exist - have you forgotten to include the key library?'); } // Draw the labelsAbove labels this.drawLabelsAbove(); // Add the dataset nodes and the event listener if (properties.tooltipsDataset) { this.addDatasetTooltip(); } // Add the event listener that clears the highlight if // there is any. Must be MOUSEDOWN (ie before the click event) var obj = this; document.body.addEventListener('mousedown', function (e) { RGraph.SVG.removeHighlight(obj); }, false); // // Allow the addition of custom text via the // text: property. // RGraph.SVG.addCustomText(this); // Draw any custom lines that have been defined RGraph.SVG.drawHorizontalLines(this); // Lastly - install the zoom event listeners if // requested if (this.properties.zoom) { RGraph.SVG.addZoom(this); } // // Fire the onfirstdraw event // if (this.firstDraw) { this.firstDraw = false; RGraph.SVG.fireCustomEvent(this, 'onfirstdraw'); } // Fire the draw event RGraph.SVG.fireCustomEvent(this, 'ondraw'); // // Install any inline responsive configuration. This // should be last in the draw function - even after // the draw events. // RGraph.SVG.installInlineResponsive(this); return this; }; // // New create() shortcut function // For example: // this.create('rect,x:0,y:0,width:100,height:100'[,parent]); // // @param str string The tag definition to parse and create // @param object The (optional) parent element // // @return object The new tag // this.create = function (str) { var def = RGraph.SVG.create.parseStr(this, str); def.svg = this.svg; // By default the parent is the SVG tag - but if // requested then change it to the tag that has // been given if (arguments[1]) { def.parent = arguments[1]; } return RGraph.SVG.create(def); }; // // Draws the bars // this.drawLine = function (data, index) { var coords = [], path = []; // Generate the coordinates for (var i=0,len=data.length; i<len; ++i) { var val = data[i], x = (( (this.graphWidth - properties.marginInner - properties.marginInner) / (len - 1) ) * i) + properties.marginLeft + properties.marginInner, y = this.getYCoord(val); coords.push([x,y]); } // Go through the coordinates and create the path that draws the line for (var i=0; i<coords.length; ++i) { if (i === 0 || RGraph.SVG.isNullish(data[i]) || RGraph.SVG.isNullish(data[i - 1])) { var action = 'M'; } else { // STEPPED Add extra lines if (properties.stepped) { path.push('L {1} {2}'.format( coords[i][0], coords[i - 1][1] )); } var action = 'L'; } path.push(action + '{1} {2}'.format( coords[i][0], RGraph.SVG.isNullish(data[i]) ? 0 : coords[i][1] )); } // // Add the coordinates to the coords array, coords2 array and if // necessary, the coordsSpline array // // The coords array for (var k=0; k<coords.length; ++k) { this.coords.push(RGraph.SVG.arrayClone(coords[k], true)); this.coords[this.coords.length - 1].x = coords[k][0]; this.coords[this.coords.length - 1].y = coords[k][1]; this.coords[this.coords.length - 1].object = this; this.coords[this.coords.length - 1].value = data[k]; this.coords[this.coords.length - 1].index = k; this.coords[this.coords.length - 1].path = path; } // The coords2 array this.coords2[index] = RGraph.SVG.arrayClone(coords, true); for (var k=0; k<coords.length; ++k) { //Get the sequential index var seq = RGraph.SVG.groupedIndexToSequential({ object: this, dataset: index, index: k }); this.coords2[index][k].x = coords[k][0]; this.coords2[index][k].y = coords[k][1]; this.coords2[index][k].object = this; this.coords2[index][k].value = data[k]; this.coords2[index][k].index = k; this.coords2[index][k].path = path; this.coords2[index][k].sequential = seq; // Draw the errorbar if required if (properties.errorbars) { this.drawErrorbar({ object: this, dataset: index, index: k, sequential: seq, x: x, y: y }); } } // The coordsSpline array if (properties.spline) { this.coordsSpline[index] = this.drawSpline(coords); } // If the line should be filled, draw the fill part if (properties.filled === true || (typeof properties.filled === 'object' && properties.filled[index]) ) { if (properties.spline) { var fillPath = ['M{1} {2}'.format( this.coordsSpline[index][0][0], this.coordsSpline[index][0][1] )]; for (var i=1; i<this.coordsSpline[index].length; ++i) { fillPath.push('L{1} {2}'.format( this.coordsSpline[index][i][0] + ((i === (this.coordsSpline[index].length) - 1) ? 1 : 0), this.coordsSpline[index][i][1] )); } } else { var fillPath = RGraph.SVG.arrayClone(path, true); } // Draw a line down to the X axis fillPath.push('L{1} {2}'.format( this.coords2[index][this.coords2[index].length - 1][0] + 1, index > 0 && properties.filledAccumulative ? (properties.spline ? this.coordsSpline[index - 1][this.coordsSpline[index - 1].length - 1][1] : this.coords2[index - 1][this.coords2[index - 1].length - 1][1]) : this.getYCoord(properties.yaxisScaleMin > 0 ? properties.yaxisScaleMin : 0) + (properties.xaxis ? 0 : 1) )); if (index > 0 && properties.filledAccumulative) { var path2 = RGraph.SVG.arrayClone(path, true); if (index > 0 && properties.filledAccumulative) { if (properties.spline) { for (var i=this.coordsSpline[index - 1].length-1; i>=0; --i) { fillPath.push('L{1} {2}'.format( this.coordsSpline[index - 1][i][0], this.coordsSpline[index - 1][i][1] )); } } else { for (var i=this.coords2[index - 1].length-1; i>=0; --i) { fillPath.push('L{1} {2}'.format( this.coords2[index - 1][i][0], this.coords2[index - 1][i][1] )); // For STEPPED charts if (properties.stepped && i > 0) { fillPath.push('L{1} {2}'.format( this.coords2[index - 1][i][0], this.coords2[index - 1][i - 1][1] )); } } } } } else { // This is the bottom left corner. The +1 is so that // the fill doesn't go over the axis fillPath.push('L{1} {2}'.format( this.coords2[index][0][0] + (properties.yaxis ? 0 : 0), // this.coords2[index][0][0] + (properties.yaxis ? 1 : 0), this.getYCoord(properties.yaxisScaleMin > 0 ? properties.yaxisScaleMin : 0) + (properties.xaxis ? 0 : 1) )); } // Find the first none-null value and use that // values X value fillPath.push('L{1} {2}'.format( this.coords2[index][0][0] + (properties.yaxis ? 1 : 0), this.coords2[index][0][1] )); for (var i=0; i<this.data[index].length; ++i) { if (!RGraph.SVG.isNullish(this.data[index][i])) { fillPath.push('L{1} {2}'.format( this.coords2[index][i][0], this.getYCoord(0) )); break; } } // Create a group that the fill is added to. Later the line // will also be added to it this.filledGroups[index] = RGraph.SVG.create({ svg: this.svg, type: 'g', parent: this.svgAllGroup, attr: { 'class': 'rgraph_filled_line_' + index } }); // Add the fill path to the scene var fillPathObject = RGraph.SVG.create({ svg: this.svg, parent: this.filledGroups[index], type: 'path', attr: { d: fillPath.join(' '), stroke: 'rgba(0,0,0,0)', 'fill': properties.filledColors && properties.filledColors[index] ? properties.filledColors[index] : properties.colors[index], 'fill-opacity': properties.filledOpacity, 'stroke-width': 1, 'clip-path': this.isTrace ? 'url(#trace-effect-clip)' : '' } }); if (properties.filledClick) { var obj = this; fillPathObject.addEventListener('click', function (e) { properties.filledClick(e, obj, index); }, false); fillPathObject.addEventListener('mousemove', function (e) { e.target.style.cursor = 'pointer'; }, false); } } // // Create the drop shadow effect if its required // if (properties.shadow) { RGraph.SVG.setShadow({ object: this, offsetx: properties.shadowOffsetx, offsety: properties.shadowOffsety, blur: properties.shadowBlur, color: properties.shadowColor, id: 'dropShadow' }); } // Add the path to the scene if (properties.spline) { // Make the raw coords into a path var str = ['M{1} {2}'.format( this.coordsSpline[index][0][0], this.coordsSpline[index][0][1] )]; for (var i=1; i<this.coordsSpline[index].length; ++i) { str.push('L{1} {2}'.format( this.coordsSpline[index][i][0], this.coordsSpline[index][i][1] )); } str = str.join(' '); var line = RGraph.SVG.create({ svg: this.svg, parent: properties.filled ? this.filledGroups[index] : this.svgAllGroup, type: 'path', attr: { d: str, stroke: properties['colors'][index], 'fill':'none', 'stroke-width': this.hasMultipleDatasets && properties.filled && properties.filledAccumulative ? 0.1 : (RGraph.SVG.isArray(properties.linewidth) ? properties.linewidth[index] : properties.linewidth + 0.01), 'stroke-dasharray': properties.dasharray ? properties.dasharray : '', 'stroke-linecap': this.getLinecap({index: index}), 'stroke-linejoin': this.getLinejoin({index: index}), filter: properties.shadow ? 'url(#dropShadow)' : '', 'clip-path': this.isTrace ? 'url(#trace-effect-clip)' : '' } }); } else { var path2 = RGraph.SVG.arrayClone(path, true); if (properties.filled && properties.filledAccumulative && index > 0) { for (var i=this.coords2[index - 1].length-1; i>=0; --i) { path2.push('L{1} {2}'.format( this.coords2[index - 1][i][0], this.coords2[index - 1][i][1] )); } } path2 = path2.join(' '); var line = RGraph.SVG.create({ svg: this.svg, parent: properties.filled ? this.filledGroups[index] : this.svgAllGroup, type: 'path', attr: { d: path2, stroke: properties.colors[index], 'fill':'none', 'stroke-dasharray': properties.dasharray ? properties.dasharray : '', 'stroke-width': this.hasMultipleDatasets && properties.filled && properties.filledAccumulative ? 0.1 : (RGraph.SVG.isArray(properties.linewidth) ? properties.linewidth[index]: properties.linewidth + 0.01), 'stroke-linecap': this.getLinecap({index: index}), 'stroke-linejoin': this.getLinejoin({index: index}), filter: properties.shadow ? 'url(#dropShadow)' : '', 'clip-path': this.isTrace ? 'url(#trace-effect-clip)' : '' } }); } if (properties.tooltips && properties.tooltips.length) { if (!document.getElementsByClassName('rgraph_hotspots').length) { var group = RGraph.SVG.create({ svg: this.svg, // Taken out on 11/12/17 so that hotspots // can sit in this group // // Put back in on 4/2/2024 so that clipping // works correctly // parent: this.svgAllGroup, type: 'g', attr: { fill: 'transparent', className: "rgraph_hotspots" }, style: { cursor: 'pointer' } }); // Store the group so it can be easily got // to later on this.svgAllGroup.line_tooltip_hotspots = group; } else { group = this.svgAllGroup.line_tooltip_hotspots; } //for (var i=0; i<this.coords2[index].length; ++i,++this.tooltipsSequentialIndex) { for (var i=0; i<this.coords2[index].length && (typeof properties.tooltips === 'string' ? true : this.tooltipsSequentialIndex < properties.tooltips.length); ++i,++this.tooltipsSequentialIndex ) { if (!RGraph.SVG.isNullish(this.originalData[index][i]) && (properties.tooltips[this.tooltipsSequentialIndex] || typeof properties.tooltips === 'string') && this.coords2[index][i][0] && this.coords2[index][i][1]) { var hotspot = RGraph.SVG.create({ svg: this.svg, parent: this.svgAllGroup, type: 'circle', attr: { cx: this.coords2[index][i][0], cy: this.coords2[index][i][1], r: properties.tooltipsHotspotSize, fill: 'transparent', 'data-dataset': index, 'data-index': i }, style: { cursor: 'pointer' } }); var obj = this; (function (sequentialIndex) { hotspot.addEventListener(properties.tooltipsEvent, function (e) { var indexes = RGraph.SVG.sequentialIndexToGrouped(sequentialIndex, obj.data), index = indexes[1], dataset = indexes[0]; if (RGraph.SVG.REG.get('tooltip') && RGraph.SVG.REG.get('tooltip').__index__ === index && RGraph.SVG.REG.get('tooltip').__dataset__ === dataset && RGraph.SVG.REG.get('tooltip').__object__.uid === obj.uid) { // Added on the 27th/6/2019 return; } obj.removeHighlight(); RGraph.SVG.hideTooltip(); RGraph.SVG.tooltip({ object: obj, index: index, dataset: dataset, sequentialIndex: sequentialIndex, text: typeof properties.tooltips === 'string' ? properties.tooltips : properties.tooltips[sequentialIndex], event: e }); // Highlight the chart here var outer_highlight1 = RGraph.SVG.create({ svg: obj.svg, parent: obj.svgAllGroup, type: 'circle', attr: { cx: obj.coords2[dataset][index][0], cy: obj.coords2[dataset][index][1], r: 5, fill: obj.properties.colors[dataset], 'fill-opacity': 0.5 }, style: { cursor: 'pointer' } }); var outer_highlight2 = RGraph.SVG.create({ svg: obj.svg, parent: obj.svgAllGroup, type: 'circle', attr: { cx: obj.coords2[dataset][index][0], cy: obj.coords2[dataset][index][1], r: 14, fill: 'white', 'fill-opacity': 0.75 }, style: { cursor: 'pointer' } }); var inner_highlight1 = RGraph.SVG.create({ svg: obj.svg, parent: obj.svgAllGroup, type: 'circle', attr: { cx: obj.coords2[dataset][index][0], cy: obj.coords2[dataset][index][1], r: 6, fill: 'white' }, style: { cursor: 'pointer' } }); var inner_highlight2 = RGraph.SVG.create({ svg: obj.svg, parent: obj.svgAllGroup, type: 'circle', attr: { cx: obj.coords2[dataset][index][0], cy: obj.coords2[dataset][index][1], r: 5, fill: typeof obj.properties.highlightFill === 'string' ? obj.properties.highlightFill : obj.properties.colors[dataset] }, style: { cursor: 'pointer' } }); // Set the highlight in the registry RGraph.SVG.REG.set('highlight', [ outer_highlight1, outer_highlight2, inner_highlight1, inner_highlight2 ]); }, false); })(this.tooltipsSequentialIndex); } } } }; // // Draws tickmarks // // @param number index The index of the line/dataset of coordinates // @param object data The origvinal line data points // @param object coords The coordinates of the points // this.drawTickmarks = function (index, data, coords) { var style = typeof properties.tickmarksStyle === 'object' ? properties.tickmarksStyle[index] : properties.tickmarksStyle, size = typeof properties.tickmarksSize === 'object' ? properties.tickmarksSize[index] : properties.tickmarksSize, fill = typeof properties.tickmarksFill === 'object' ? properties.tickmarksFill[index] : properties.tickmarksFill, linewidth = typeof properties.tickmarksLinewidth === 'object' ? properties.tickmarksLinewidth[index] : properties.tickmarksLinewidth; for (var i=0; i<data.length; ++i) { if (typeof data[i] === 'number') { switch (style) { case 'filledcircle': case 'filledendcircle': if (style === 'filledcircle' || (i === 0 || i === data.length - 1) ) { var circle = RGraph.SVG.create({ svg: this.svg, parent: this.svgAllGroup, type: 'circle', attr: { cx: coords[index][i][0], cy: coords[index][i][1], r: size, 'fill': properties.colors[index], filter: properties.shadow? 'url(#dropShadow)' : '', 'clip-path': this.isTrace ? 'url(#trace-effect-clip)' : '' }, style: { pointerEvents: 'none' } }); } break; case 'circle': case 'endcircle': if (style === 'circle' || (style === 'endcircle' && (i === 0 || i === data.length - 1)) ) { var outerCircle = RGraph.SVG.create({ svg: this.svg, parent: this.svgAllGroup, type: 'circle', attr: { cx: coords[index][i][0], cy: coords[index][i][1], r: size + this.get('linewidth'), 'fill': properties.colors[index], filter: properties.shadow? 'url(#dropShadow)' : '', 'clip-path': this.isTrace ? 'url(#trace-effect-clip)' : '' }, style: { pointerEvents: 'none' } }); var innerCircle = RGraph.SVG.create({ svg: this.svg, parent: this.svgAllGroup, type: 'circle', attr: { cx: coords[index][i][0], cy: coords[index][i][1], r: size, 'fill': fill, 'clip-path': this.isTrace ? 'url(#trace-effect-clip)' : '' }, style: { pointerEvents: 'none' } }); break; } break; case 'endrect': case 'rect': if (style === 'rect' || (style === 'endrect' && (i === 0 || i === data.length - 1)) ) { var fill = typeof fill === 'object'&& typeof fill[index] === 'string' ? fill[index] : fill; var rect = RGraph.SVG.create({ svg: this.svg, parent: this.svgAllGroup, type: 'rect', attr: { x: coords[index][i][0] - size, y: coords[index][i][1] - size, width: size + size + linewidth, height: size + size + linewidth, 'stroke-width': this.get('linewidth'), 'stroke': properties.colors[index], 'fill': fill, 'clip-path': this.isTrace ? 'url(#trace-effect-clip)' : '' }, style: { pointerEvents: 'none' } }); } break; case 'filledendrect': case 'filledrect': if (style === 'filledrect' || (style === 'filledendrect' && (i === 0 || i === data.length - 1)) ) { var fill = properties.colors[index]; var rect = RGraph.SVG.create({ svg: this.svg, parent: this.svgAllGroup, type: 'rect', attr: { x: coords[index][i][0] - size, y: coords[index][i][1] - size, width: size + size + linewidth, height: size + size + linewidth, 'fill': fill, 'clip-path': this.isTrace ? 'url(#trace-effect-clip)' : '' }, style: { pointerEvents: 'none' } }); } } } } }; // // Redraws the line in certain circumstances: // o filled // o filledAccumulative // o Multiple lines // this.redrawLines = function () { if (properties.spline) { for (var i=0; i<this.coordsSpline.length; ++i) { var linewidth = RGraph.SVG.isArray(properties.linewidth) ? properties.linewidth[i] : properties.linewidth, color = properties['colors'][i], path = ''; // Create the path from the spline coordinates for (var j=0; j<this.coordsSpline[i].length; ++j) { if (j === 0) { path += 'M{1} {2} '.format( this.coordsSpline[i][j][0], this.coordsSpline[i][j][1] ); } else { path += 'L{1} {2} '.format( this.coordsSpline[i][j][0], this.coordsSpline[i][j][1] ); } } RGraph.SVG.create({ svg: this.svg, parent: properties.filled ? this.filledGroups[i] : this.svgAllGroup, type: 'path', attr: { d: path, stroke: color, 'fill':'none', 'stroke-dasharray': properties.dasharray ? properties.dasharray : '', 'stroke-width': linewidth + 0.01, 'stroke-linecap': this.getLinecap({index: i}), 'stroke-linejoin': this.getLinejoin({index: i}), filter: properties.shadow ? 'url(#dropShadow)' : '', 'clip-path': this.isTrace ? 'url(#trace-effect-clip)' : '' } }); } // Now draw the tickmarks for (var dataset=0; dataset<this.coords2.length; ++dataset) { this.drawTickmarks( dataset, this.data[dataset], this.coords2 ); } } else { for (var i=0; i<this.coords2.length; ++i) { var linewidth = RGraph.SVG.isArray(properties.linewidth) ? properties.linewidth[i] : properties.linewidth, color = properties['colors'][i], path = ''; // Create the path from the coordinates for (var j=0; j<this.coords2[i].length; ++j) { if (j === 0 || RGraph.SVG.isNullish(this.data[i][j]) || RGraph.SVG.isNullish(this.data[i][j - 1])) { path += 'M{1} {2} '.format( this.coords2[i][j][0], RGraph.SVG.isNullish(this.data[i][j]) ? 0 : this.coords2[i][j][1] ); } else { if (properties.stepped) { path += 'L{1} {2} '.format( this.coords2[i][j][0], this.coords2[i][j - 1][1] ); } path += 'L{1} {2} '.format( this.coords2[i][j][0], this.coords2[i][j][1] ); } } RGraph.SVG.create({ svg: this.svg, parent: properties.filled ? this.filledGroups[i] : this.svgAllGroup, type: 'path', attr: { d: path, stroke: color, 'fill':'none', 'stroke-dasharray': properties.dasharray ? properties.dasharray : '', 'stroke-width': linewidth + 0.01, 'stroke-linecap': this.getLinecap({index: i}), 'stroke-linejoin': this.getLinejoin({index: i}), filter: properties.shadow ? 'url(#dropshadow)' : '', 'clip-path': this.isTrace ? 'url(#trace-effect-clip)' : '' } }); } // Now draw the tickmarks for (var dataset=0; dataset<this.coords2.length; ++dataset) { this.drawTickmarks( dataset, this.data[dataset], this.coords2 ); } } }; // // This function can be used to retrieve the relevant Y coordinate for a // particular value. // // @param int value The value to get the Y coordinate for // this.getYCoord = function (value) { // You can now give a boolean true value to stipulate // that outofbounds values are allowed if (arguments[1] === true) { var allowOutOfBounds = true; } var y; if (!allowOutOfBounds && RGraph.SVG.isNullish(value)) { return null; } if (!allowOutOfBounds && value > this.scale.max) { return null; } if (!allowOutOfBounds && value < this.scale.min) { return null; } y = ((value - this.scale.min) / (this.scale.max - this.scale.min)); y *= (this.height - properties.marginTop - properties.marginBottom); y = this.height - properties.marginBottom - y; return y; }; // // This function can be used to highlight a bar on the chart // // TODO This function looks like its needs updating // // @param object rect The rectangle to highlight // this.highlight = function (rect) { var x = rect.getAttribute('x'), y = rect.getAttribute('y'); }; // // Remove highlight from the chart (tooltips) // this.removeHighlight = function () { //var highlight = RGraph.SVG.REG.get('highlight'); //if (highlight && highlight.parentNode) { // highlight.parentNode.removeChild(highlight); // //// The highlight is an array //} else if (highlight && RGraph.SVG.isArray(highlight)) { // for (var i=0; i<highlight.length; ++i) { // if (highlight[i] && highlight[i].parentNode) { // highlight[i].parentNode.removeChild(highlight[i]); // } // } //} //RGraph.SVG.REG.set('highlight', null); RGraph.SVG.removeHighlight(); }; // // Draw a spline Line chart // // @param array coords The coords for the line // this.drawSpline = function (coords) { var xCoords = []; marginLeft = properties.marginLeft, marginRight = properties.marginRight, hmargin = properties.marginInner, interval = (this.graphWidth - (2 * hmargin)) / (coords.length - 1), coordsSpline = []; // // The drawSpline function takes an array of JUST Y coords - not X/Y coords. So the line coords need converting // if we've been given X/Y pairs // for (var i=0,len=coords.length; i<len;i+=1) { if (typeof coords[i] == 'object' && coords[i] && coords[i].length == 2) { coords[i] = Number(coords[i][1]); } } // // Get the Points array in the format we want - the first value should // be null along with the lst value // var P = [coords[0]]; for (var i=0; i<coords.length; ++i) { P.push(coords[i]); } P.push(coords[coords.length - 1] + (coords[coords.length - 1] - coords[coords.length - 2])); for (var j=1; j<P.length-2; ++j) { for (var t=0; t<10; ++t) { var yCoord = spline( t/10, P[j-1], P[j], P[j+1], P[j+2] ); xCoords.push(((j-1) * interval) + (t * (interval / 10)) + marginLeft + hmargin); coordsSpline.push([ xCoords[xCoords.length - 1], yCoord ]); if (typeof index === 'number') { coordsSpline[index].push([ xCoords[xCoords.length - 1], yCoord ]); } } } // Draw the last section coordsSpline.push([ ((j-1) * interval) + marginLeft + hmargin, P[j] ]); if (typeof index === 'number') { coordsSpline.push([ ((j-1) * interval) + marginLeft + hmargin, P[j] ]); } function spline (t, P0, P1, P2, P3) { return 0.5 * ((2 * P1) + ((0-P0) + P2) * t + ((2*P0 - (5*P1) + (4*P2) - P3) * (t*t) + ((0-P0) + (3*P1)- (3*P2) + P3) * (t*t*t))); } // Add some properties to the coordinates for (var i=0; i<coordsSpline.length; ++i) { coordsSpline[i].object = this; coordsSpline[i].x = this; coordsSpline[i].y = this; } return coordsSpline; }; // // This allows for easy specification of gradients // this.parseColors = function () { if (!Object.keys(this.originalColors).length) { this.originalColors = { colors: RGraph.SVG.arrayClone(properties.colors, true), filledColors: RGraph.SVG.arrayClone(properties.filledColors, true), backgroundGridColor: RGraph.SVG.arrayClone(properties.backgroundGridColor, true), //highlightFill: RGraph.SVG.arrayClone(properties.highlightFill, true), backgroundColor: RGraph.SVG.arrayClone(properties.backgroundColor, true) } } // colors var colors = properties.colors; if (colors) { for (var i=0; i<colors.length; ++i) { colors[i] = RGraph.SVG.parseColorLinear({ object: this, color: colors[i] }); } } // Fill colors var filledColors = properties.filledColors; if (filledColors) { for (var i=0; i<filledColors.length; ++i) { filledColors[i] = RGraph.SVG.parseColorLinear({ object: this, color: filledColors[i] }); } } properties.backgroundGridColor = RGraph.SVG.parseColorLinear({object: this, color: properties.backgroundGridColor}); //properties.highlightFill = RGraph.SVG.parseColorLinear({object: this, color: properties.highlightFill}); properties.backgroundColor = RGraph.SVG.parseColorLinear({object: this, color: properties.backgroundColor}); }; // // Draws the labelsAbove // this.drawLabelsAbove = function () { // Go through the above labels if (properties.labelsAbove) { var data_seq = RGraph.SVG.arrayLinearize(this.data), seq = 0; for (var dataset=0; dataset<this.coords2.length; ++dataset,seq++) { for (var i=0; i<this.coords2[dataset].length; ++i,seq++) { var str = RGraph.SVG.numberFormat({ object: this, num: this.data[dataset][i].toFixed(properties.labelsAboveDecimals ), prepend: typeof properties.labelsAboveUnitsPre === 'string' ? properties.labelsAboveUnitsPre : null, append: typeof properties.labelsAboveUnitsPost === 'string' ? properties.labelsAboveUnitsPost : null, point: typeof properties.labelsAbovePoint === 'string' ? properties.labelsAbovePoint : null, thousand: typeof properties.labelsAboveThousand === 'string' ? properties.labelsAboveThousand : null, formatter: typeof properties.labelsAboveFormatter === 'function' ? properties.labelsAboveFormatter : null }); // Facilitate labelsAboveSpecific if (properties.labelsAboveSpecific && properties.labelsAboveSpecific.length && (typeof properties.labelsAboveSpecific[seq] === 'string' || typeof properties.labelsAboveSpecific[seq] === 'number') ) { str = properties.labelsAboveSpecific[seq]; } else if ( properties.labelsAboveSpecific && properties.labelsAboveSpecific.length && typeof properties.labelsAboveSpecific[seq] !== 'string' && typeof properties.labelsAboveSpecific[seq] !== 'number') { continue; } // Get the text configuration for the above labels var textConf = RGraph.SVG.getTextConf({ object: this, prefix: 'labelsAbove' }); RGraph.SVG.text({ object: this, parent: this.svgAllGroup, tag: 'labels.above', text: str, x: parseFloat(this.coords2[dataset][i][0]) + properties.labelsAboveOffsetx, y: parseFloat(this.coords2[dataset][i][1]) + properties.labelsAboveOffsety, halign: properties.labelsAboveHalign, valign: properties.labelsAboveValign, font: textConf.font, size: textConf.size, bold: textConf.bold, italic: textConf.italic, color: textConf.color, background: properties.labelsAboveBackground || null, padding: properties.labelsAboveBackgroundPadding || 0 }); } // Necessary so that the seq doesn't get incremented twice seq--; } } }; // // Using a function to add events makes it easier to facilitate method // chaining // // @param string type The type of even to add // @param function func // this.on = function (type, func) { if (type.substr(0,2) !== 'on') { type = 'on' + type; } RGraph.SVG.addCustomEventListener(this, type, func); return this; }; // // Used in chaining. Runs a function there and then - not waiting for // the events to fire (eg the onbeforedraw event) // // @param function func The function to execute // this.exec = function (func) { func(this); return this; }; // This function is used to draw the errorbar. Its in the common // file because it's used by multiple chart libraries this.drawErrorbar = function (opt) { var linewidth = RGraph.SVG.getErrorbarsLinewidth({object: this, index: opt.index}), color = RGraph.SVG.getErrorbarsColor({object: this, index: opt.sequential}), capwidth = RGraph.SVG.getErrorbarsCapWidth({object: this, index: opt.index}), index = opt.index, dataset = opt.dataset, x = opt.x, y = opt.y, value = this.data[dataset][index]; // Get the Y coord of the point var y = this.getYCoord(y); // Get the error bar value var max = RGraph.SVG.getErrorbarsMaxValue({ object: this, index: opt.sequential }); // Get the error bar value var min = RGraph.SVG.getErrorbarsMinValue({ object: this, index: opt.sequential }); if (!max && !min) { return; } var x = this.coords2[dataset][index].x, y = this.coords2[dataset][index].y, halfCapWidth = capwidth / 2, y1 = this.getYCoord(value + max), y3 = this.getYCoord(value - min) === null ? y : this.getYCoord(value - min); if (max > 0) { // Draw the UPPER vertical line var errorbarLine = RGraph.SVG.create({ svg: this.svg, type: 'line', parent: this.svgAllGroup, attr: { x1: x, y1: y, x2: x, y2: y1, stroke: color, 'stroke-width': linewidth } }); // Draw the cap to the UPPER line var errorbarCap = RGraph.SVG.create({ svg: this.svg, type: 'line', parent: this.svgAllGroup, attr: { x1: x - halfCapWidth, y1: y1, x2: x + halfCapWidth, y2: y1, stroke: color, 'stroke-width': linewidth } }); } // Draw the minimum errorbar if necessary if (typeof min === 'number') { var errorbarLine = RGraph.SVG.create({ svg: this.svg, type: 'line', parent: this.svgAllGroup, attr: { x1: x, y1: y, x2: x, y2: y3, stroke: color, 'stroke-width': linewidth } }); // Draw the cap to the UPPER line var errorbarCap = RGraph.SVG.create({ svg: this.svg, type: 'line', parent: this.svgAllGroup, attr: { x1: x - halfCapWidth, y1: y3, x2: x + halfCapWidth, y2: y3, stroke: color, 'stroke-width': linewidth } }); } }; // // A trace effect // // @param object Options to give to the effect // @param function A function to call when the effect has completed // this.trace = function () { var opt = arguments[0] || {}, frame = 1, frames = opt.frames || 60, obj = this; this.isTrace = true; this.draw(); // Create the clip area var clippath = RGraph.SVG.create({ svg: this.svg, parent: this.svg.defs, type: 'clipPath', attr: { id: 'trace-effect-clip' } }); var clippathrect = RGraph.SVG.create({ svg: this.svg, parent: clippath, type: 'rect', attr: { x: 0, y: 0, width: 0, height: this.height } }); var iterator = function () { var width = (frame++) / frames * obj.width; clippathrect.setAttribute("width", width); if (frame <= frames) { RGraph.SVG.FX.update(iterator); } else { // Remove the clippath clippath.parentNode.removeChild(clippath); if (opt.callback) { (opt.callback)(obj); } } }; iterator(); return this; }; // // A worker function that handles Bar chart specific tooltip substitutions // this.tooltipSubstitutions = function (opt) { var indexes = RGraph.SVG.sequentialIndexToGrouped(opt.index, this.data); // Create the values array which contains each datasets value for (var i=0,values=[]; i<this.originalData.length; ++i) { values.push(this.originalData[i][indexes[1]]); } return { index: indexes[1], dataset: indexes[0], sequentialIndex: opt.index, value: typeof this.data[indexes[0]] === 'number' ? this.data[indexes[0]] : this.data[indexes[0]][indexes[1]], values: values }; }; // // This allows for static tooltip positioning // this.positionTooltipStatic = function (args) { var obj = args.object, e = args.event, tooltip = args.tooltip, index = args.index, svgXY = RGraph.SVG.getSVGXY(obj.svg), coords = this.coords[args.index]; // Position the tooltip in the X direction args.tooltip.style.left = ( svgXY[0] // The X coordinate of the canvas + coords[0] // The X coordinate of the bar on the chart - (tooltip.offsetWidth / 2) // Subtract half of the tooltip width ) + 'px'; args.tooltip.style.top = ( svgXY[1] // The Y coordinate of the canvas + coords[1] // The Y coordinate of the bar on the chart - tooltip.offsetHeight // The height of the tooltip - 15 // An arbitrary amount ) + 'px'; }; // // Draws a trendline on the Scatter chart. This is also known // as a "best-fit line" // // @param dataset The index of the dataset to use // this.drawTrendline = function (opt) { var obj = this, color = properties.trendlineColor, linewidth = properties.trendlineLinewidth, margin = properties.trendlineMargin, clip = properties.trendlineClip; // Create the clipping region if necessary if (clip) { // Create the clip area var clippath = RGraph.SVG.create({ svg: this.svg, parent: this.svg.defs, type: 'clipPath', attr: { id: 'trendline-clip' } }); var clippathrect = RGraph.SVG.create({ svg: this.svg, parent: clippath, type: 'rect', attr: { x: properties.marginLeft, y: properties.marginTop, width: this.width - properties.marginLeft - properties.marginRight, height: this.height - properties.marginTop - properties.marginBottom } }); } // // Create the pseudo-data array // var data=[]; // Create the data array from the given data and an // increasing X value for (var i=0; i<this.data.length; ++i) { data[i] = []; for (var j=0; j<this.data[i].length; ++j) { data[i].push([j, this.data[i][j]]); } } // Allow for trendlineColors as well if (RGraph.SVG.isArray(properties.trendlineColors)) { color = properties.trendlineColors; } // handle the options being arrays if (typeof color === 'object' && color[opt.dataset]) { color = color[opt.dataset]; } else if (typeof color === 'object') { color = 'gray'; } if (typeof linewidth === 'object' && typeof linewidth[opt.dataset] === 'number') { linewidth = linewidth[opt.dataset]; } else if (typeof linewidth === 'object') { linewidth = 1; } if (typeof margin === 'object' && typeof margin[opt.dataset] === 'number') { margin = margin[opt.dataset]; } else if (typeof margin === 'object'){ margin = 25; } // Step 1: Calculate the mean values of the X coords and the Y coords for (var i=0,totalX=0,totalY=0; i<this.data[opt.dataset].length; ++i) { totalX += data[opt.dataset][i][0]; totalY += data[opt.dataset][i][1]; } var averageX = totalX / data[opt.dataset].length; var averageY = totalY / data[opt.dataset].length; // Step 2: Calculate the slope of the line // a: The X/Y values minus the average X/Y value for (var i=0,xCoordMinusAverageX=[],yCoordMinusAverageY=[],valuesMultiplied=[],xCoordMinusAverageSquared=[]; i<this.data[opt.dataset].length; ++i) { xCoordMinusAverageX[i] = data[opt.dataset][i][0] - averageX; yCoordMinusAverageY[i] = data[opt.dataset][i][1] - averageY; // b. Multiply the averages valuesMultiplied[i] = xCoordMinusAverageX[i] * yCoordMinusAverageY[i]; xCoordMinusAverageSquared[i] = xCoordMinusAverageX[i] * xCoordMinusAverageX[i]; } var sumOfValuesMultiplied = RGraph.SVG.arraySum(valuesMultiplied); var sumOfXCoordMinusAverageSquared = RGraph.SVG.arraySum(xCoordMinusAverageSquared); // Calculate m (???) var m = sumOfValuesMultiplied / sumOfXCoordMinusAverageSquared; var b = averageY - (m * averageX); // y = mx + b var coords = [ [0, m * 0 + b], [data[0].length - 1, m * (data[0].length - 1) + b] ]; // Convert the X/Y numbers into coordinates coords[0][0] = properties.marginLeft; coords[0][1] = this.getYCoord(coords[0][1], true); coords[1][0] = this.width - properties.marginRight; coords[1][1] = this.getYCoord(coords[1][1], true); // // Draw the line // // Set dotted, dash or a custom dash array if ( properties.trendlineDashed === true || (RGraph.SVG.isArray(properties.trendlineDashed) && properties.trendlineDashed[opt.dataset]) ) { var dasharray = [4,4]; } if ( properties.trendlineDotted === true || (RGraph.SVG.isArray(properties.trendlineDotted) && properties.trendlineDotted[opt.dataset])) { var dasharray = [1,4]; } // Set a lineDash array. It can be an array of two numbers, or it can be a // multi-dimensional array, each of two numbers. One for each line on the // chart. if (RGraph.SVG.isArray(properties.trendlineDashArray)) { if ( properties.trendlineDashArray.length === 2 && typeof properties.trendlineDashArray[0] === 'number' && typeof properties.trendlineDashArray[1] === 'number' ) { var dasharray = properties.trendlineDashArray; } else if ( RGraph.SVG.isArray(properties.trendlineDashArray) && RGraph.SVG.isArray(properties.trendlineDashArray[opt.dataset])) { var dasharray = properties.trendlineDashArray[opt.dataset]; } } // // Store the coordinates of the trendline // this.coordsTrendline[opt.dataset] = [ [Math.max(coords[0][0], this.coords2[opt.dataset][0][0] - margin), coords[0][1]], [Math.min(coords[1][0], this.coords2[opt.dataset][this.coords2[opt.dataset].length - 1][0] + margin), coords[1][1]] ]; // // Draw the line // var line = RGraph.SVG.create({ svg: obj.svg, parent: obj.svgAllGroup, type: 'line', attr: { x1: this.coordsTrendline[opt.dataset][0][0],//Math.max(coords[0][0], this.coords2[opt.dataset][0][0] - margin), y1: this.coordsTrendline[opt.dataset][0][1],//coords[0][1], x2: this.coordsTrendline[opt.dataset][1][0],//Math.min(coords[1][0], this.coords2[opt.dataset][this.coords2[opt.dataset].length - 1][0] + margin), y2: this.coordsTrendline[opt.dataset][1][1],//coords[1][1], fill: 'rgba(0,0,0,0)', stroke: color, 'stroke-width': linewidth, 'stroke-dasharray': dasharray ? dasharray : '', 'class': 'rgraph_line_{1}_trendline_{2}'.format( this.id, opt.dataset ), 'clip-path': clip ? 'url(#trendline-clip)' : ''//, //'clip-path': this.isTrace ? 'url(#trace-effect-clip)' : '' } }); // Get a reference to the background gride <path> node var els = this.svg.getElementsByClassName('rgraph_background_grid'), grid = els[0]; // Remove the trendline from the DOM obj.svgAllGroup.removeChild(line); // Now re-add it immedately after the background grid grid.insertAdjacentElement('afterend', line); }; // // This is the code the adds lines across null gaps in the // Line chart // // @param number datasetIdx The index of the dataset // @param array data The dataset // this.nullBridge = function (datasetIdx, data) { var readData = false; // // Now add the connecting lines // for (var i=0; i<data.length; i++) { var isNull = false, start = null, end = null; // This ensures that the first datapoint is not null if (readData === false && RGraph.SVG.isNumber(data[i])) { readData = true; } if (RGraph.SVG.isNullish(data[i]) && readData) { start = i - 1; for (var j=(i+1); j<data.length; ++j) { if (RGraph.SVG.isNullish(data[j])) { continue; } else { end = j; } // No idea why this if() condition is necessary but it // prevents an error occurring when the coordinate is // null if (this.coords2[datasetIdx][start][1]) { var path = 'M{1} {2} L{3} {4}'.format( this.coords2[datasetIdx][start][0], this.coords2[datasetIdx][start][1], this.coords2[datasetIdx][end][0], this.coords2[datasetIdx][end][1], ); } // Create the path and add it to the SVG document var node = RGraph.SVG.create({ svg: this.svg, parent: this.svgAllGroup, type: 'path', attr: { d: path, stroke: typeof properties.nullBridgeColors === 'string' ? properties.nullBridgeColors : ((typeof properties.nullBridgeColors === 'object' && !RGraph.SVG.isNullish(properties.nullBridgeColors) && properties.nullBridgeColors[datasetIdx]) ? properties.nullBridgeColors[datasetIdx] : properties.colors[datasetIdx]), 'fill': 'transparent', 'stroke-dasharray': properties.nullBridgeDashArray ? properties.nullBridgeDashArray : '', 'stroke-width': typeof properties.nullBridgeLinewidth === 'number' ? properties.nullBridgeLinewidth : properties.linewidth, 'stroke-linecap': this.getLinecap({index: i}), 'stroke-linejoin': this.getLinejoin({index: i}), 'clip-path': this.isTrace ? 'url(#trace-effect-clip)' : '' } }); start = null; end = null; break; } } } }; // // Add the dataset tooltip event listener // this.addDatasetTooltip = function () { if (this.properties.spline) { var coords = this.coordsSpline; } else { var coords = this.coords2; } for (let i=0; i<coords.length; ++i) { // Create the path that is placed on // top of the line that facilitates // the click // var path = RGraph.SVG.create.pathString(coords[i]); // // If the line chart is filled then create // an extra path that goes back over the // previous line in reverse. When doing // this for dataset 0 then there is no // previous dataset so just go back to the // right using the X axis coordinates // if ( (properties.filled && properties.filledAccumulative && i === 0) || (properties.filled && !properties.filledAccumulative) ) { path += 'L {1} {2}'.format( this.width - properties.marginRight - properties.marginInner, this.height - properties.marginBottom ); path += 'L {1} {2}'.format( properties.marginLeft + properties.marginInner, this.height - properties.marginBottom ); // Chart is filled, but this is not the // first dataset. So go back over the // previous dataset coordinates to get get // to the LHS } else if (properties.filled && properties.filledAccumulative && i > 0) { var previous_dataset = coords[i-1]; path += RGraph.SVG.create.pathString(previous_dataset.toReversed(), 'L'); } // Create the line the goes over the line as a // cover var node = RGraph.SVG.create({ svg: this.svg, type: 'path', parent: this.svgAllGroup, attr: { fill: properties.filled ? '#0000' : 'none', d: path, stroke: '#0000', 'stroke-width': properties.filled ? 0 : Math.max(5, properties.linewidth), 'stroke-linecap':'round' }, style: { cursor: 'pointer' } }); // // Now add the dataset event listener that causes // the dataset toolotip to be shown // var obj = this; (function (dataset) { node.addEventListener(properties.tooltipsEvent, function (e) { if (RGraph.SVG.REG.get('tooltip') && RGraph.SVG.REG.get('tooltip').__dataset__ === dataset && RGraph.SVG.REG.get('tooltip').__object__.uid === obj.uid) { // Added on the 27th/6/2019 return; } // Lose any previous highlighting var previous_highlight = RGraph.SVG.REG.get('tooltip-dataset-highlight'); if (previous_highlight) { previous_highlight.setAttribute(obj.properties.filled ? 'fill' : 'stroke', 'transparent'); RGraph.SVG.REG.set('tooltip-dataset-highlight', null); } // // Calculate a sequential index to give // the tooltip // var seq = 0; for (let i=0; i<dataset; ++i) { seq += obj.coords2[i].length; } seq += (obj.coords2[dataset].length / 2); RGraph.SVG.tooltip({ object: obj, index: Math.round(obj.coords2[dataset].length / 2), dataset: dataset, sequentialIndex: Math.floor(seq), text: typeof properties.tooltipsDataset === 'string' ? properties.tooltipsDataset : properties.tooltipsDataset[dataset], event: e }); // Highlight the line // if (properties.filled) { e.target.setAttribute('fill','#fff9'); } e.target.setAttribute('stroke','#fff9'); RGraph.SVG.REG.set('tooltip-dataset-highlight', e.target); RGraph.SVG.runOnce('tooltip-dataset-window-mousedown-event-listener-gfyugyuyugfugfyu', function () { window.addEventListener('mousedown', function (e) { RGraph.SVG.hideTooltip(); if (RGraph.SVG.REG.get('tooltip-dataset-highlight')) { RGraph.SVG.REG.get('tooltip-dataset-highlight').setAttribute('stroke', 'transparent'); RGraph.SVG.REG.get('tooltip-dataset-highlight').setAttribute('fill', properties.filled ? 'transparent' : 'none'); } }); }); }, false); })(i); } }; // // Sets the linecap style // Not always very noticeable, but these do have an effect // with thick lines // // butt square round // this.getLinecap = function (opt) { if (typeof properties.linecap === 'object' && typeof properties.linecap[opt.index] === 'string') { return properties.linecap[opt.index]; } else if ( typeof properties.linecap === 'string' ) { return properties.linecap; } else { return 'round'; } }; // // Sets the linejoin style // // round miter bevel // this.getLinejoin = function (opt) { if (typeof properties.linejoin === 'object' && typeof properties.linejoin[opt.index] === 'string') { return properties.linejoin[opt.index]; } else if ( typeof properties.linejoin === 'string' ) { return properties.linejoin; } else { return 'round'; } }; // // This function handles clipping to scale values. Because // each chart handles scales differently, a worker function // is needed instead of it all being done centrally. // // @param object clipPath The <clipPath> node // this.clipToScaleWorker = function (clipPath) { // The Regular expression is actually done by the // calling RGraph.clipTo.start() function in the core // library if (RegExp.$1 === 'min') from = this.min; else from = Number(RegExp.$1); if (RegExp.$2 === 'max') to = this.max; else to = Number(RegExp.$2); var width = this.width, y1 = this.getYCoord(from), y2 = this.getYCoord(to), height = Math.abs(y2 - y1), x = 0, y = Math.min(y1, y2); // Increase the height if the maximum value is "max" if (RegExp.$2 === 'max') { y = 0; height += this.properties.marginTop; } // Increase the height if the minimum value is "min" if (RegExp.$1 === 'min') { height += this.properties.marginBottom; } RGraph.SVG.create({ svg: this.svg, type: 'rect', parent: clipPath, attr: { x: x, y: y, width: width, height: height } }); // Now set the clip-path attribute on the first // Line charts all-elements group this.svgAllGroup.setAttribute( 'clip-path', 'url(#' + clipPath.id + ')' ); }; // // Set the options that the user has provided // for (i in conf.options) { if (typeof i === 'string') { this.set(i, conf.options[i]); } } }; // // This is a "wrapper" function that creaters a dual-color // trendline Line chart for you. Options to give to // the frunction are (the sole argument is an object): // // id: The id of the canvas tag // data: The data for the chart // options: The chart options that get applied to // both Line charts (the one above // and also the one below the trendline.) // optionsTop: With this option you can specify // configuration values that are specific to // the top chart (eg color) // optionsBottom: With this option you can specify // configuration values that are specific to // the bottom chart (eg color) // RGraph.SVG.Line.dualColorTrendline = function (args) { // Check that a trendline is enabled if(!args.options.trendline) { alert('[ALERT] A trendline is not enabled in your charts configuration'); return; } // // Draw the red part of the Scatter chart (the bottom // half) // var obj1 = new RGraph.SVG.Line({ id: args.id, data: RGraph.SVG.arrayClone(args.data, true), options: RGraph.SVG.arrayClone(args.options, true) }).draw(); // The coordinates of the first (and only) trendline var coords = obj1.coordsTrendline[0]; // // Calculate the coordinates for the top part of the chart // (above the trendline) // var coords_top = [ [0,coords[0][1]], ...coords, [obj1.width,coords[1][1]], [obj1.width, 0], [0,0] ]; // // Calculate the coordinates for the bottom part of the chart // (below the trendline) // var coords_bottom = [ [0,coords[0][1]], ...coords, [obj1.width,coords[1][1]], [obj1.width, obj1.height], [0,obj1.height] ]; // // Now that we have the coordinates, clipping can be // installed on the chart that's already been drawn // (the top part of the chart). // obj1.set('clip', coords_top); // Set any options that have been specified that are // specific to the top Scatter chart if (RGraph.SVG.isObject(args.optionsTop)) { for (i in args.optionsTop) { if (RGraph.SVG.isString(i)) { obj1.set(i, args.optionsTop[i]); } } } // // Create a new chart that's clipped to the bottom part // coordinates. // var obj2 = new RGraph.SVG.Line({ id: args.id, data: RGraph.SVG.arrayClone(args.data, true), options: { ...RGraph.SVG.arrayClone(args.options, true), clip: coords_bottom // Clip to the part of the canvas // that's below the trendline } }); // Set any options that have been specified that are // specific to the bottom Scatter chart if (RGraph.SVG.isObject(args.optionsBottom)) { for (i in args.optionsBottom) { if (RGraph.SVG.isString(i)) { obj2.set(i, args.optionsBottom[i]); } } } // // Now draw both of the charts using the RGraph.redraw // API function or the requested animation effect if ( RGraph.SVG.isString(args.animationEffect) && obj1[args.animationEffect] && obj1[args.animationEffect] ) { RGraph.SVG.clear(obj1.svg); var effect = args.animationEffect; var effectOptions = args.animationEffectOptions ? args.animationEffectOptions : null; effectOptions.callback = function () { RGraph.SVG.runOnce('rgraph-svg-line-dual-color-effect-callback', function () { args.animationEffectCallback ? args.animationEffectCallback() : function () {}; }); } obj1[effect](effectOptions); obj2[effect](effectOptions); } else { RGraph.SVG.redraw(); } return [obj1, obj2]; }; return this; // End module pattern })(window, document);