// 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}; // // The line chart constructor // RGraph.Line = function (conf) { var id = conf.id, canvas = document.getElementById(id), data = conf.data; this.id = id; this.canvas = canvas; this.context = this.canvas.getContext('2d'); this.canvas.__object__ = this; this.type = 'line'; this.max = 0; this.coords = []; this.coords2 = []; this.coords.key = []; this.coordsText = []; this.coordsSpline = []; this.coordsAxes = {xaxis: [], yaxis: []}; this.coordsTrendline = []; this.hasnegativevalues = false; this.isRGraph = true; this.isrgraph = true; this.rgraph = true; this.uid = RGraph.createUID(); this.canvas.uid = this.canvas.uid ? this.canvas.uid : RGraph.createUID(); this.colorsParsed = false; this.original_colors = []; this.firstDraw = true; // After the first draw this will be false this.stopAnimationRequested = false;// Used to control the animations this.growEffectMultiplier = 1; // Used by the Grow effect // Various config type stuff this.properties = { backgroundBarsCount: null, backgroundBarsColor1: 'rgba(0,0,0,0)', backgroundBarsColor2: 'rgba(0,0,0,0)', backgroundGrid: 1, backgroundGridLinewidth: 1, backgroundGridHsize: 25, backgroundGridVsize: 25, backgroundGridColor: '#ddd', backgroundGridVlines: true, backgroundGridHlines: true, backgroundGridBorder: true, backgroundGridAutofit: true, backgroundGridAutofitAlign: true, backgroundGridHlinesCount: 5, backgroundGridVlinesCount: null, backgroundGridDashed: false, backgroundGridDotted: false, backgroundGridDashArray: null, backgroundHbars: null, backgroundImage: null, backgroundImageStretch: true, backgroundImageX: null, backgroundImageY: null, backgroundImageW: null, backgroundImageH: null, backgroundImageAlign: null, backgroundColor: null, backgroundBorder: false, backgroundBorderLinewidth: 1, backgroundBorderColor: '#aaa', backgroundBorderDashed: false, backgroundBorderDotted: false, backgroundBorderDashArray: null, xaxis: true, xaxisLinewidth: 1, xaxisColor: 'black', xaxisTickmarks: true, xaxisTickmarksLength: 3, xaxisTickmarksLastLeft: null, xaxisTickmarksLastRight: null, xaxisTickmarksCount: null, xaxisLabels: null, xaxisLabelsFormattedDecimals: 0, xaxisLabelsFormattedPoint: '.', xaxisLabelsFormattedThousand: ',', xaxisLabelsFormattedUnitsPre: '', xaxisLabelsFormattedUnitsPost: '', xaxisLabelsSize: null, xaxisLabelsFont: null, xaxisLabelsItalic: null, xaxisLabelsBold: null, xaxisLabelsColor: null, xaxisLabelsOffsetx: 0, xaxisLabelsOffsety: 0, xaxisLabelsHalign: null, xaxisLabelsValign: null, xaxisLabelsPosition: 'edge', xaxisLabelsSpecificAlign:'left', xaxisPosition: 'bottom', xaxisPosition: 'bottom', xaxisLabelsAngle: 0, xaxisTitle: '', xaxisTitleBold: null, xaxisTitleSize: null, xaxisTitleFont: null, xaxisTitleColor: null, xaxisTitleItalic: null, xaxisTitlePos: null, xaxisTitleOffsetx: 0, xaxisTitleOffsety: 0, xaxisTitleX: null, xaxisTitleY: null, xaxisTitleHalign: 'center', xaxisTitleValign: 'top', yaxis: true, yaxisLinewidth: 1, yaxisColor: 'black', yaxisTickmarks: true, yaxisTickmarksCount: null, yaxisTickmarksLastTop: null, yaxisTickmarksLastBottom: null, yaxisTickmarksLength: 3, yaxisScale: true, yaxisScaleMin: 0, yaxisScaleMax: null, yaxisScaleUnitsPre: '', yaxisScaleUnitsPost: '', yaxisScaleDecimals: 0, yaxisScalePoint: '.', yaxisScaleThousand: ',', yaxisScaleRound: false, yaxisScaleFormatter: null, yaxisScaleInvert: false, yaxisLabelsSpecific: null, yaxisLabelsCount: 5, yaxisLabelsOffsetx: 0, yaxisLabelsOffsety: 0, yaxisLabelsHalign: null, yaxisLabelsValign: null, yaxisLabelsFont: null, yaxisLabelsSize: null, yaxisLabelsColor: null, yaxisLabelsBold: null, yaxisLabelsItalic: null, yaxisLabelsPosition: 'edge', yaxisPosition: 'left', yaxisTitle: '', yaxisTitleBold: null, yaxisTitleSize: null, yaxisTitleFont: null, yaxisTitleColor: null, yaxisTitleItalic: null, yaxisTitlePos: null, yaxisTitleX: null, yaxisTitleY: null, yaxisTitleOffsetx: 0, yaxisTitleOffsety: 0, yaxisTitleHalign: null, yaxisTitleValign: null, yaxisTitleAccessible: null, labelsAbove: false, labelsAboveDecimals: null, labelsAboveSize: null, labelsAboveColor: null, labelsAboveFont: null, labelsAboveBold: null, labelsAboveItalic: null, labelsAboveBackground: 'rgba(255,255,255,0.7)', labelsAboveBorder: false, labelsAboveUnitsPre: '', labelsAboveUnitsPost: '', labelsAboveSpecific: null, labelsAboveOffsetx: 0, labelsAboveOffsety: 0, labelsAboveFormatter: null, linewidth: 2.001, linecap: 'round', linejoin: 'round', colors: ['red', '#0f0', '#00f', '#f0f', '#ff0', '#0ff','green','pink','blue','black'], tickmarksStyle: 'none', tickmarksLinewidth: null, tickmarksSize: 3, tickmarksColor: null, tickmarksStyleDotStroke: 'white', tickmarksStyleDotFill: null, tickmarksStyleDotLinewidth: 3, tickmarksStyleImage: null, tickmarksStyleImageHalign: 'center', tickmarksStyleImageValign: 'center', tickmarksStyleImageOffsetx: 0, tickmarksStyleImageOffsety: 0, marginLeft: 35, marginRight: 35, marginTop: 35, marginBottom: 35, marginInner: 0, filledColors: null, textBold: false, textItalic: false, textSize: 12, textColor: 'black', textFont: "Arial, Verdana, sans-serif", textAccessible: false, textAccessibleOverflow: 'visible', textAccessiblePointerevents:false, text: null, title: '', titleFont: null, titleSize: null, titleColor: null, titleBold: null, titleItalic: null, titleX: null, titleY: null, titleHalign: null, titleValign: null, titleOffsetx: 0, titleOffsety: 0, titleSubtitle: '', titleSubtitleSize: null, titleSubtitleColor: '#aaa', titleSubtitleFont: null, titleSubtitleBold: null, titleSubtitleItalic: null, titleSubtitleOffsetx: 0, titleSubtitleOffsety: 0, shadow: true, shadowOffsetx: 2, shadowOffsety: 2, shadowBlur: 3, shadowColor: 'rgba(128,128,128,0.5)', tooltips: null, tooltipsHotspotXonly: false, tooltipsHotspotSize: 5, tooltipsHotspotIgnore: null, tooltipsEffect: 'slide', tooltipsCssClass: 'RGraph_tooltip', tooltipsCss: null, tooltipsEvent: 'onmousemove', tooltipsHighlight: true, tooltipsPersistent: false, tooltipsCoordsPage: false, tooltipsFormattedThousand: ',', tooltipsFormattedPoint: '.', tooltipsFormattedDecimals: 0, tooltipsFormattedUnitsPre: '', tooltipsFormattedUnitsPost: '', tooltipsFormattedKeyColors: null, tooltipsFormattedKeyColorsShape: 'square', tooltipsFormattedKeyLabels: [], tooltipsFormattedListType: 'ul', tooltipsFormattedListItems: null, tooltipsFormattedTableHeaders: null, tooltipsFormattedTableData: null, tooltipsDataset: null, tooltipsDatasetEvent: 'click', tooltipsPointer: true, tooltipsPointerOffsetx: 0, tooltipsPointerOffsety: 0, tooltipsPositionStatic: true, highlightStyle: null, highlightStroke: 'gray', highlightFill: 'white', highlightPointRadius: 2, highlightDataset: false, highlightDatasetStroke: 'rgba(0,0,0,0.25)', highlightDatasetFill: 'rgba(255,255,255,0.75)', highlightDatasetStrokeAlpha: 1, highlightDatasetFillAlpha: 1, highlightDatasetFillUseColors: false, highlightDatasetStrokeUseColors: false, highlightDatasetLinewidth: null, highlightDatasetDotted: false, highlightDatasetDashed: false, highlightDatasetDashArray: null, highlightDatasetExclude: null, highlightDatasetCallback: null, highlightDatasetEvent: 'click', stepped: false, key: null, keyBackground: 'white', keyPosition: 'graph', keyHalign: null, keyShadow: false, keyShadowColor: '#666', keyShadowBlur: 3, keyShadowOffsetx: 2, keyShadowOffsety: 2, keyPositionMarginBoxed: false, keyPositionMarginHSpace: 0, keyPositionX: null, keyPositionY: null, keyColorShape: 'square', keyRounded: true, keyLinewidth: 1, keyColors: null, keyInteractive: false, keyInteractiveHighlightChartStroke: 'rgba(255,0,0,0.3)', keyInteractiveHighlightLabel: 'rgba(255,0,0,0.2)', keyLabelsFont: null, keyLabelsSize: null, keyLabelsColor: null, keyLabelsBold: null, keyLabelsItalic: null, keyLabelsOffsetx: 0, keyLabelsOffsety: 0, keyFormattedDecimals: 0, keyFormattedPoint: '.', keyFormattedThousand: ',', keyFormattedUnitsPre: '', keyFormattedUnitsPost: '', keyFormattedValueSpecific: null, keyFormattedItemsCount: null, contextmenu: null, crosshairs: false, crosshairsColor: '#333', crosshairsLinewidth: 1, crosshairsHline: true, crosshairsVline: true, crosshairsSnapToScale: false, annotatable: false, annotatableColor: 'black', annotatableLinewidth: 1, filled: false, filledRange: false, filledRangeThreshold: null, filledRangeThresholdColors: ['red', 'green'], filledAccumulative: true, variant: null, axesAbove: false, backdrop: false, backdropSize: 30, backdropAlpha: 0.2, adjustable: false, adjustableOnly: null, adjustableXonly: false, redraw: true, outofbounds: false, outofboundsClip: false, animationFactor: 1, animationUnfoldX: false, animationUnfoldY: true, animationUnfoldInitial: 2, animationTraceClip: 1, animationTraceCenter: false, spline: false, lineVisible: [], errorbars: false, errorbarsColor: 'black', errorbarsCapped: true, errorbarsCappedWidth: 12, errorbarsLinewidth: 1, combinedEffect: null, combinedEffectOptions: null, combinedEffectCallback: null, clearto: 'rgba(0,0,0,0)', dotted: false, dashed: false, 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: [5,5], labelsAngled: false, labelsAngledSpecific: null, labelsAngledAccessible: null, labelsAngledFont: null, labelsAngledColor: null, labelsAngledSize: null, labelsAngledBold: null, labelsAngledItalic: null, labelsAngledUpFont: null, labelsAngledUpColor: null, labelsAngledUpSize: null, labelsAngledUpBold: null, labelsAngledUpItalic: null, labelsAngledDownFont: null, labelsAngledDownColor: null, labelsAngledDownSize: null, labelsAngledDownBold: null, labelsAngledDownItalic: null, labelsAngledLevelFont: null, labelsAngledLevelColor: null, labelsAngledLevelSize: null, labelsAngledLevelBold: null, labelsAngledILeveltalic:null, lines: null // Used to show an average line indicator (for example) } // // 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; } } // Convert strings to numbers this.original_data = RGraph.stringsToNumbers(conf.data); // // Store the original data. This also allows for giving arguments as one big array. // if (typeof this.original_data[0] === 'number' || RGraph.isNullish(this.original_data[0])) { this.original_data = [RGraph.arrayClone(this.original_data)]; } // Some of the animations actually modify the // .original_data array so we need to keep a // copy of the unmodified, unmodified data. this // variable is used to access that data in those // effects (eg the wave effect). this.unmodified_data = RGraph.arrayClone(this.original_data); // Check for support if (!this.canvas) { alert('[LINE] Fatal error: no canvas support'); return; } // // Store the data here as one big array // this.data_arr = RGraph.arrayLinearize(this.original_data); for (var i=0; i<this.data_arr.length; ++i) { this['$' + i] = {}; } // Easy access to properties and the path function var properties = this.properties; this.path = RGraph.pathObjectFunction; // // "Decorate" the object with the generic effects if the effects library has been included // if (RGraph.Effects && typeof RGraph.Effects.decorate === 'function') { RGraph.Effects.decorate(this); } // Add the responsive method. This method resides in the common file. this.responsive = RGraph.responsive; // // An all encompassing accessor // this.set = function (name) { var value = typeof arguments[1] === 'undefined' ? null : arguments[1]; // Go through all of the properties and make sure // that they're using the correct capitalisation if (typeof name === 'string') { name = this.properties_lowercase_map[name.toLowerCase()] || name; } // Set the colorsParsed flag to false if the colors // property is being set if ( name === 'colors' || name === 'fillledColors' || name === 'keyColors' || name === 'backgroundHbars' || name === 'backgroundBarsColor1' || name === 'backgroundBarsColor2' || name === 'backgroundGridColor' || name === 'backgroundColor' || name === 'textColor' || name === 'crosshairsColor' || name === 'annotatableColor' || name === 'titleColor' || name === 'xaxisTitleColor' || name === 'yaxisTitleColor' || name === 'keyBackground' || name === 'axesColor' || name === 'highlightFill' ) { this.colorsParsed = false; } // the number of arguments is only one and it's an // object - parse it for configuration data and return. if (arguments.length === 1 && typeof arguments[0] === 'object') { for (i in arguments[0]) { if (typeof i === 'string') { this.set(i, arguments[0][i]); } } return this; } properties[name] = value; return this; }; // // An all encompassing accessor // // @param string name The name of the property // 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 properties[name]; }; // // The function you call to draw the line chart // this.draw = function () { // MUST be the first thing done! if (typeof properties.backgroundImage == 'string') { RGraph.drawBackgroundImage(this); } // // Fire the onbeforedraw event // RGraph.fireCustomEvent(this, 'onbeforedraw'); // Translate half a pixel for antialiasing purposes - but only if it hasn't been // done already // // MUST be the first thing done! // if (!this.canvas.__rgraph_aa_translated__) { this.context.translate(0.5,0.5); this.canvas.__rgraph_aa_translated__ = true; } // // Parse the colors. This allows for simple gradient syntax // if (!this.colorsParsed) { this.parseColors(); // Don't want to do this again this.colorsParsed = true; } // // Make the margins easy to access // this.marginLeft = properties.marginLeft; this.marginRight = properties.marginRight; this.marginTop = properties.marginTop; this.marginBottom = properties.marginBottom; if (properties.xaxisLabels && properties.xaxisLabels.length) { // // If the xaxisLabels option is a string then turn it // into an array. // if (typeof properties.xaxisLabels === 'string') { properties.xaxisLabels = RGraph.arrayPad({ array: [], length: this.original_data[0].length, value: properties.xaxisLabels }); } for (var i=0; i<properties.xaxisLabels.length; ++i) { properties.xaxisLabels[i] = RGraph.labelSubstitution({ object: this, text: properties.xaxisLabels[i], index: i, value: this.original_data[0][i], decimals: properties.xaxisLabelsFormattedDecimals || 0, unitsPre: properties.xaxisLabelsFormattedUnitsPre || '', unitsPost: properties.xaxisLabelsFormattedUnitsPost || '', thousand: properties.xaxisLabelsFormattedThousand || ',', point: properties.xaxisLabelsFormattedPoint || '.' }); } } // Reset the data back to that which was initially supplied this.data = RGraph.arrayClone(this.original_data); // Reset the max value this.max = 0; if (properties.filled && !properties.filledRange && this.data.length > 1 && properties.filledAccumulative) { var accumulation = []; for (var set=0; set<this.data.length; ++set) { for (var point=0; point<this.data[set].length; ++point) { this.data[set][point] = Number(accumulation[point] ? accumulation[point] : 0) + this.data[set][point]; accumulation[point] = this.data[set][point]; } } } // // Get the maximum Y scale value // if ( properties.yaxisScaleMax) { this.max = properties.yaxisScaleMax; this.min = properties.yaxisScaleMin ? properties.yaxisScaleMin : 0; this.scale2 = RGraph.getScale({object: this, options: { 'scale.max': this.max, 'scale.min': properties.yaxisScaleMin, 'scale.strict': true, 'scale.thousand': properties.yaxisScaleThousand, 'scale.point': properties.yaxisScalePoint, 'scale.decimals': properties.yaxisScaleDecimals, 'scale.labels.count': properties.yaxisLabelsCount, 'scale.round': properties.yaxisScaleRound, 'scale.units.pre': properties.yaxisScaleUnitsPre, 'scale.units.post': properties.yaxisScaleUnitsPost }}); this.max = this.scale2.max ? this.scale2.max : 0; // Check for negative values if (!properties.outofbounds) { for (dataset=0; dataset<this.data.length; ++dataset) { if (RGraph.isArray(this.data[dataset])) { for (var datapoint=0; datapoint<this.data[dataset].length; datapoint++) { // Check for negative values this.hasnegativevalues = (this.data[dataset][datapoint] < 0) || this.hasnegativevalues; } } } } } else { this.min = properties.yaxisScaleMin ? properties.yaxisScaleMin : 0; // Work out the max Y value for (dataset=0; dataset<this.data.length; ++dataset) { for (var datapoint=0; datapoint<this.data[dataset].length; datapoint++) { this.max = Math.max(this.max, this.data[dataset][datapoint] ? Math.abs(parseFloat(this.data[dataset][datapoint])) : 0); // Check for negative values if (!properties.outofbounds) { this.hasnegativevalues = (this.data[dataset][datapoint] < 0) || this.hasnegativevalues; } } } this.scale2 = RGraph.getScale({object: this, options: { 'scale.max': this.max, 'scale.min': properties.yaxisScaleMin, 'scale.thousand': properties.yaxisScaleThousand, 'scale.point': properties.yaxisScalePoint, 'scale.decimals': properties.yaxisScaleDecimals, 'scale.labels.count': properties.yaxisLabelsCount, 'scale.round': properties.yaxisScaleRound, 'scale.units.pre': properties.yaxisScaleUnitsPre, 'scale.units.post': properties.yaxisScaleUnitsPost, 'scale.formatter': properties.yaxisScaleFormatter }}); this.max = this.scale2.max ? this.scale2.max : 0; } // // Setup the context menu if required // if (properties.contextmenu) { RGraph.showContext(this); } // // Reset the coords arrays otherwise it will keep growing // this.coords = []; this.coordsText = []; // // Work out a few things. They need to be here because they depend on things you can change before you // call Draw() but after you instantiate the object // this.grapharea = this.canvas.height - this.marginTop - this.marginBottom; this.halfgrapharea = this.grapharea / 2; this.halfTextHeight = properties.textSize / 2; // // Install clipping before anything is drawn but after // everything has been calculated (eg maximum values // etc) // if (!RGraph.isNullish(this.properties.clip)) { RGraph.clipTo.start(this, this.properties.clip); } if (properties.variant == '3d') { RGraph.draw3DAxes(this); } // Draw the background RGraph.Background.draw(this); // // Draw any horizontal bars that have been defined // if (properties.backgroundHbars && properties.backgroundHbars.length > 0) { RGraph.drawBars(this); } if (!properties.axesAbove) { this.drawAxes(); } // // This facilitates the new Trace2 effect // this.context.save() this.context.beginPath(); // The clipping region is idfferent based on th animationTraceCenter option if (properties.animationTraceCenter) { this.context.rect( (this.canvas.width / 2) * (1 - properties.animationTraceClip), 0, this.canvas.width * properties.animationTraceClip, this.canvas.height ); } else { this.context.rect(0, 0, this.canvas.width * properties.animationTraceClip, this.canvas.height); } this.context.clip(); for (var i=0, j=0, len=this.data.length; i<len; i++, j++) { this.context.beginPath(); // // Turn on the shadow if required // if (!properties.filled) { this.setShadow(i); } // // Draw the line // if (properties.filledColors) { if (typeof properties.filledColors == 'object' && properties.filledColors[j]) { var fill = properties.filledColors[j]; } else if (typeof properties.filledColors == 'object' && properties.filledColors.toString().indexOf('Gradient') > 0) { var fill = properties.filledColors; } else if (typeof properties.filledColors == 'string') { var fill = properties.filledColors; } } else if (properties.filled) { var fill = properties.colors[j]; } else { var fill = null; } // // Figure out the tickmark to use // if (properties.tickmarksStyle && typeof properties.tickmarksStyle == 'object') { var tickmarks = properties.tickmarksStyle[i]; } else if (properties.tickmarksStyle && typeof properties.tickmarksStyle == 'string') { var tickmarks = properties.tickmarksStyle; } else if (properties.tickmarksStyle && typeof properties.tickmarksStyle == 'function') { var tickmarks = properties.tickmarksStyle; } else { var tickmarks = null; } // // Draw the line, accounting for the outofboundsClip option // if (properties.outofboundsClip) { this.path( 'sa b r % % % % cl b', 0,this.marginTop,this.canvas.width,this.canvas.height - this.marginTop - this.marginBottom ); } this.drawLine( this.data[i], properties.colors[j], fill, this.getLineWidth(j), tickmarks, i ); if (properties.outofboundsClip) { this.context.restore(); } this.context.stroke(); } // // If the line is filled re-stroke the lines // if (properties.outofboundsClip) { this.path( 'sa b r % % % % cl b', 0,this.marginTop,this.canvas.width,this.canvas.height - this.marginTop - this.marginBottom ); } if (properties.filled && properties.filledAccumulative && !properties.spline) { for (var i=0; i<this.coords2.length; ++i) { this.context.beginPath(); this.context.lineWidth = this.getLineWidth(i); this.context.strokeStyle = !this.hidden(i) ? properties.colors[i] : 'rgba(0,0,0,0)'; for (var j=0,len=this.coords2[i].length; j<len; ++j) { if (j == 0 || this.coords2[i][j][1] == null || (this.coords2[i][j - 1] && this.coords2[i][j - 1][1] == null)) { this.context.moveTo(this.coords2[i][j][0], this.coords2[i][j][1]); } else { if (properties.stepped) { this.context.lineTo(this.coords2[i][j][0], this.coords2[i][j - 1][1]); } this.context.lineTo(this.coords2[i][j][0], this.coords2[i][j][1]); } } this.context.stroke(); // No fill! } // Redraw the tickmarks if (properties.tickmarksStyle) { this.context.beginPath(); this.context.fillStyle = 'white'; for (var i=0,len=this.coords2.length; i<len; ++i) { this.context.beginPath(); this.context.strokeStyle = properties.colors[i]; for (var j=0; j<this.coords2[i].length; ++j) { if (typeof this.coords2[i][j] == 'object' && typeof this.coords2[i][j][0] == 'number' && typeof this.coords2[i][j][1] == 'number') { var tickmarks = typeof properties.tickmarksStyle == 'object' ? properties.tickmarksStyle[i] : properties.tickmarksStyle; this.drawTick( this.coords2[i], this.coords2[i][j][0], this.coords2[i][j][1], this.context.strokeStyle, false, j == 0 ? 0 : this.coords2[i][j - 1][0], j == 0 ? 0 : this.coords2[i][j - 1][1], tickmarks, j, i ); } } } this.context.stroke(); this.context.fill(); } } else if (properties.filled && properties.filledAccumulative && properties.spline) { // Restroke the curvy filled accumulative lines for (var i=0; i<this.coordsSpline.length; i+=1) { this.context.beginPath(); this.context.strokeStyle = properties.colors[i]; this.context.lineWidth = this.getLineWidth(i); for (var j=0,len=this.coordsSpline[i].length; j<len; j+=1) { var point = this.coordsSpline[i][j]; j == 0 ? this.context.moveTo(point[0], point[1]) : this.context.lineTo(point[0], point[1]); } this.context.stroke(); } for (var i=0,len=this.coords2.length; i<len; i+=1) { for (var j=0,len2=this.coords2[i].length; j<len2; ++j) { if (typeof this.coords2[i][j] == 'object' && typeof this.coords2[i][j][0] == 'number' && typeof this.coords2[i][j][1] == 'number') { var tickmarks = typeof properties.tickmarksStyle == 'object' && !RGraph.isNullish(properties.tickmarksStyle) ? properties.tickmarksStyle[i] : properties.tickmarksStyle; this.context.strokeStyle = properties.colors[i]; this.drawTick( this.coords2[i], this.coords2[i][j][0], this.coords2[i][j][1], properties.colors[i], false, j == 0 ? 0 : this.coords2[i][j - 1][0], j == 0 ? 0 : this.coords2[i][j - 1][1], tickmarks, j, i ); } } } } // // Bridge the null gaps if requested // // This was moved on 25/028/2021 to be inside the clip() // so that the trace() animation works // if (properties.nullBridge) { for (var i=0; i<this.data.length; ++i) { this.nullBridge(i, this.data[i]); } } if (properties.outofboundsClip) { this.context.restore(); } this.context.restore(); // ??? this.context.beginPath(); // // If the axes have been requested to be on top, do that // if (properties.axesAbove) { this.drawAxes(); } // // Draw the labels // this.drawLabels(); // // Draw the range if necessary // this.drawRange(); // Draw a key if necessary if (properties.key && properties.key.length && RGraph.drawKey) { RGraph.drawKey(this, properties.key, properties.colors); } // // Draw " above" labels if enabled // if (properties.labelsAbove) { this.drawAboveLabels(); } // // Draw the "in graph" labels // RGraph.drawInGraphLabels(this); // // Redraw the lines if a filled range is on the cards // if (properties.filled && properties.filledRange && this.data.length == 2) { this.context.beginPath(); var len = this.coords.length / 2; this.context.lineWidth = properties.linewidth; this.context.strokeStyle = this.hidden(0) ? 'rgba(0,0,0,0)' : properties.colors[0]; for (var i=0; i<len; ++i) { if (!RGraph.isNullish(this.coords[i][1])) { if (i == 0) { this.context.moveTo(this.coords[i][0], this.coords[i][1]); } else { this.context.lineTo(this.coords[i][0], this.coords[i][1]); } } } this.context.stroke(); this.context.beginPath(); if ( properties.colors[1]) { this.context.strokeStyle = this.hidden(1) ? 'rgba(0,0,0,0)' : properties.colors[1]; } for (var i=this.coords.length - 1; i>=len; --i) { if (!RGraph.isNullish(this.coords[i][1])) { if (i == (this.coords.length - 1)) { this.context.moveTo(this.coords[i][0], this.coords[i][1]); } else { this.context.lineTo(this.coords[i][0], this.coords[i][1]); } } } this.context.stroke(); } else if (properties.filled && properties.filledRange) { alert('[LINE] You must have only two sets of data for a filled range chart'); } // Add trendlines if they have been enabled for (var i=0; i<this.data.length; ++i) { if ( (RGraph.isArray(properties.trendline) && properties.trendline[i]) || (!RGraph.isArray(properties.trendline) && properties.trendline)) { this.context.save() this.context.beginPath(); // The clipping region is idfferent based on the // animationTraceCenter option if (properties.animationTraceCenter) { this.context.rect( (this.canvas.width / 2) * (1 - properties.animationTraceClip), 0, this.canvas.width * properties.animationTraceClip, this.canvas.height ); } else { this.context.rect(0, 0, this.canvas.width * properties.animationTraceClip, this.canvas.height); } this.context.clip(); this.drawTrendline(i); this.context.restore(); } } // // Add custom text thats specified // RGraph.addCustomText(this); // // This installs the event listeners // RGraph.installEventListeners(this); // // Add the tooltipsDataset tooltips listener // if (properties.tooltipsDataset) { this.addDatasetTooltip(); } // Draw any custom lines that have been defined RGraph.drawHorizontalLines(this); // // End clipping // RGraph.clipTo.end(); // // Fire the onfirstdraw event // if (this.firstDraw) { this.firstDraw = false; RGraph.fireCustomEvent(this, 'onfirstdraw'); this.firstDrawFunc(); } // // Fire the RGraph draw event // RGraph.fireCustomEvent(this, 'ondraw'); // // Install any inline responsive configuration. This // should be last in the draw function - even after // the draw events. // RGraph.installInlineResponsive(this); 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; }; // // Draws the axes // this.drawAxes = function () { this.context.beginPath(); this.context.lineCap = 'square'; // Draw the X axis RGraph.drawXAxis(this); // Draw the Y axis RGraph.drawYAxis(this); // // This is here so that setting the color after this function doesn't // change the color of the axes // this.context.beginPath(); }; // Draw the text labels for the axes this.drawLabels = function () { // Now done by the X and Y axis functions }; // // Draws the line // this.drawLine = function (lineData, color, fill, linewidth, tickmarks, index) { // This facilitates the Rise animation (the Y value only) if (properties.animationUnfoldY && properties.animationFactor != 1) { for (var i=0; i<lineData.length; ++i) { lineData[i] *= properties.animationFactor; } } var penUp = false; var yPos = null; var xPos = 0; this.context.lineWidth = 1; var lineCoords = []; // // Get the previous line data // if (index > 0) { var prevLineCoords = this.coords2[index - 1]; } this.setLinecap({index: index}); this.setLinejoin({index: index}); // Work out the X interval var xInterval = (this.canvas.width - (2 * properties.marginInner) - this.marginLeft - this.marginRight) / (lineData.length - 1); // Loop thru each value given, plotting the line // (FORMERLY FIRST) for (i=0,len=lineData.length; i<len; i+=1) { var data_point = lineData[i]; // For the Grow effect!! data_point *= this.growEffectMultiplier; // // Get the yPos for the given data point // var yPos = this.getYCoord(data_point); // Null data points, and a special case a bug if ( lineData[i] == null || ( properties.xaxisPosition == 'bottom' && lineData[i] < this.min && !properties.outofbounds) || ( properties.xaxisPosition == 'center' && lineData[i] < (-1 * this.max) && !properties.outofbounds) || (((lineData[i] < this.min && properties.xaxisPosition !== 'center') || lineData[i] > this.max) && !properties.outofbounds)) { yPos = null; } // Plot the line if we're at least on the second iteration if (i > 0) { xPos = xPos + xInterval; } else { xPos = properties.marginInner + this.marginLeft; } if (properties.animationUnfoldX) { xPos *= properties.animationFactor; if (xPos < properties.marginLeft) { xPos = properties.marginLeft; } } // // Add the coords to an array // this.coords.push([xPos, yPos]); lineCoords.push([xPos, yPos]); } this.context.stroke(); // Store the coords in another format, indexed by line number this.coords2[index] = lineCoords; // // Now draw the actual line [FORMERLY SECOND] // this.context.beginPath(); // Transparent now as of 11/19/2011 this.context.strokeStyle = 'rgba(0,0,0,0)'; //this.context.strokeStyle = fill; if (fill) { this.context.fillStyle = fill; } var isStepped = properties.stepped; var isFilled = properties.filled; if ( properties.xaxisPosition == 'top') { var xAxisPos = this.marginTop; } else if ( properties.xaxisPosition == 'center') { var xAxisPos = this.marginTop + (this.grapharea / 2); } else if ( properties.xaxisPosition == 'bottom') { var xAxisPos = this.getYCoord( properties.yaxisScaleMin) } for (var i=0,len=lineCoords.length; i<len; i+=1) { xPos = lineCoords[i][0]; yPos = lineCoords[i][1]; var set = index; var prevY = (lineCoords[i - 1] ? lineCoords[i - 1][1] : null); var isLast = (i + 1) == lineCoords.length; // // This nullifys values which are out-of-range // if (!properties.outofbounds && (prevY < this.marginTop || prevY > (this.canvas.height - this.marginBottom) ) ) { penUp = true; } if (i == 0 || penUp || !yPos || !prevY || prevY < this.marginTop) { if (properties.filled && !properties.filledRange) { if (!properties.outofbounds || prevY === null || yPos === null) { this.context.moveTo(xPos + 1, xAxisPos); } // This facilitates the X axis being at the top // NOTE: Also done below if ( properties.xaxisPosition == 'top') { this.context.moveTo(xPos + 1, xAxisPos); } if (isStepped && i > 0) { this.context.lineTo(xPos, lineCoords[i - 1][1]); } this.context.lineTo(xPos, yPos); } else { if (RGraph.ISOLD && yPos == null) { // Nada } else { this.context.moveTo(xPos + 1, yPos); } } if (yPos == null) { penUp = true; } else { penUp = false; } } else { // Draw the stepped part of stepped lines if (isStepped) { this.context.lineTo(xPos, lineCoords[i - 1][1]); } if ((yPos >= this.marginTop && yPos <= (this.canvas.height - this.marginBottom)) || properties.outofbounds ) { if (isLast && properties.filled && !properties.filledRange && properties.yaxisPosition == 'right') { xPos -= 1; } // Added 8th September 2009 if (!isStepped || !isLast) { this.context.lineTo(xPos, yPos); if (isFilled && lineCoords[i+1] && lineCoords[i+1][1] == null) { this.context.lineTo(xPos, xAxisPos); } // Added August 2010 } else if (isStepped && isLast) { this.context.lineTo(xPos,yPos); } penUp = false; } else { penUp = true; } } } // // Draw a line to the X axis if the chart is filled // if (properties.filled && !properties.filledRange && !properties.spline) { // Is this needed ?? var fillStyle = properties.filledColors; // // Draw the bottom edge of the filled bit using either the X axis or the prevlinedata, // depending on the index of the line. The first line uses the X axis, and subsequent // lines use the prevLineCoords array // if (index > 0 && properties.filledAccumulative) { this.context.lineTo(xPos, prevLineCoords ? prevLineCoords[i - 1][1] : (this.canvas.height - this.marginBottom - 1 + ( properties.xaxisPosition == 'center' ? (this.canvas.height - this.marginTop - this.marginBottom) / 2 : 0))); for (var k=(i - 1); k>=0; --k) { this.context.lineTo(k == 0 ? prevLineCoords[k][0] + 1: prevLineCoords[k][0], prevLineCoords[k][1]); } } else { // Draw a line down to the X axis if ( properties.xaxisPosition == 'top') { this.context.lineTo(xPos, properties.marginTop + 1); this.context.lineTo(lineCoords[0][0], properties.marginTop + 1); } else if (typeof lineCoords[i - 1][1] == 'number') { var yPosition = this.getYCoord(0); this.context.lineTo(xPos,yPosition); this.context.lineTo(lineCoords[0][0],yPosition); } } this.context.fillStyle = !this.hidden(index) ? fill : 'rgba(0,0,0,0)'; this.context.fill(); this.context.beginPath(); } this.context.stroke(); if (properties.backdrop) { this.drawBackdrop(lineCoords, color); } // // TODO CLIP TRACE // By using the clip() method the Trace animation can be updated. // NOTE: Needs to be done for the filled part as well // NOTE: This appears to have been done? // this.context.save(); this.context.beginPath(); // The clipping region is different based on th animationTraceCenter option if (properties.animationTraceCenter) { this.context.rect( (this.canvas.width / 2) * (1 - properties.animationTraceClip), 0, this.canvas.width * properties.animationTraceClip, this.canvas.height ); } else { this.context.rect(0, 0, this.canvas.width * properties.animationTraceClip, this.canvas.height); } this.context.clip(); // // Draw errorbars // if (typeof properties.errorbars !== 'null') { this.drawErrorbars(); } // Now redraw the lines with the correct line width this.setShadow(index); this.redrawLine(lineCoords, color, linewidth, index); this.context.stroke(); RGraph.noShadow(this); // Draw the tickmarks for (var i=0; i<lineCoords.length; ++i) { i = Number(i); // // Set the color // this.context.strokeStyle = color; if (isStepped && i == (lineCoords.length - 1)) { this.context.beginPath(); //continue; } if ( ( tickmarks != 'endcircle' && tickmarks != 'endsquare' && tickmarks != 'filledendsquare' && tickmarks != 'endtick' && tickmarks != 'endtriangle' && tickmarks != 'arrow' && tickmarks != 'filledarrow' ) || (i == 0 && tickmarks != 'arrow' && tickmarks != 'filledarrow') || i == (lineCoords.length - 1) ) { var prevX = (i <= 0 ? null : lineCoords[i - 1][0]); var prevY = (i <= 0 ? null : lineCoords[i - 1][1]); this.drawTick( lineData, lineCoords[i][0], lineCoords[i][1], color, false, prevX, prevY, tickmarks, i, index ); } } this.context.restore(); // // Draw the undulating labels that follow // the line up and down // this.drawAngledLabels(); // Draw something off canvas to skirt an annoying bug this.context.beginPath(); this.context.arc(this.canvas.width + 50000, this.canvas.height + 50000, 2, 0, 6.38, 1); }; // // This functions draws a tick mark on the line // this.drawTick = function (lineData, xPos, yPos, color, isShadow, prevX, prevY, tickmarks, index, dataset) { // Reset the linedash setting for drawing the tickmarks //this.context.setLineDash([1,1]); this.context.setLineDash([]); // Allow for the tickmarksColor property if (properties.tickmarksColor) { color = properties.tickmarksColor; } // Various conditions mean no tick if (this.hidden(dataset)) { return; } else if (RGraph.isNullish(yPos)) { return false; } else if ((yPos > (this.canvas.height - this.marginBottom)) && !properties.outofbounds) { return; } else if ((yPos < this.marginTop) && !properties.outofbounds) { return; } this.context.beginPath(); var offset = 0; // Reset the stroke and lineWidth back to the same as what they were when the line was drawn // UPDATE 28th July 2011 - the line width is now set to 1 this.path( 'lw % ss % fs %', properties.tickmarksLinewidth ? properties.tickmarksLinewidth : properties.linewidth, isShadow ? properties.shadowColor : color, isShadow ? properties.shadowColor : color ); // Cicular tick marks if ( tickmarks == 'circle' || tickmarks == 'round' || tickmarks == 'filledcircle' || tickmarks == 'endcircle' || tickmarks === 'filledendcircle') { if (tickmarks == 'round'|| tickmarks == 'circle'|| tickmarks == 'filledcircle' || ((tickmarks == 'endcircle' || tickmarks === 'filledendcircle') && (index == 0 || index == (lineData.length - 1)))) { this.path( 'b a % % % % % %', xPos + offset,yPos + offset,properties.tickmarksSize,0,360 / (180 / RGraph.PI),false ); if (tickmarks.indexOf('filled') !== -1) { this.path( 'fs %', isShadow ? properties.shadowColor : color ); } else { this.path( 'fs %', isShadow ? properties.shadowColor : 'white' ); } this.context.fill(); this.context.stroke(); } // Halfheight "Line" style tick marks } else if (tickmarks == 'halftick') { this.path( 'b m % % l % % s null', Math.round(xPos), yPos, Math.round(xPos), yPos + properties.tickmarksSize ); // Tick style tickmarks } else if (tickmarks == 'tick') { this.path( 'b m % % l % % s', Math.round(xPos), yPos - properties.tickmarksSize, Math.round(xPos), yPos + properties.tickmarksSize ); // Endtick style tickmarks } else if (tickmarks == 'endtick' && (index == 0 || index == (lineData.length - 1))) { this.path( 'b m % % l % % s', Math.round(xPos), yPos - properties.tickmarksSize, Math.round(xPos), yPos + properties.tickmarksSize ); // "Cross" style tick marks } else if (tickmarks == 'cross') { var ticksize = properties.tickmarksSize; this.path( 'b m % % l % % m % % l % % s %', xPos - ticksize, yPos - ticksize, xPos + ticksize, yPos + ticksize, xPos + ticksize, yPos - ticksize, xPos - ticksize, yPos + ticksize, color ); // Triangle style tick marks } else if (tickmarks == 'triangle' || tickmarks == 'filledtriangle' || (tickmarks == 'endtriangle' && (index == 0 || index == (lineData.length - 1)))) { this.path( 'b m % % l % % l % % c f % s null', Math.round(xPos - properties.tickmarksSize), yPos + properties.tickmarksSize, Math.round(xPos), yPos - properties.tickmarksSize, Math.round(xPos + properties.tickmarksSize), yPos + properties.tickmarksSize, tickmarks === 'filledtriangle' ? (isShadow ? properties.shadowColor : this.context.strokeStyle) : 'white' ); // // A white bordered circle // } else if (tickmarks == 'borderedcircle' || tickmarks == 'dot') { this.path( 'lw % b a % % % % % false c f % s %', properties.tickmarksStyleDotLinewidth || 0.00000001, xPos, yPos, properties.tickmarksSize, 0, 360 / (180 / RGraph.PI), properties.tickmarksStyleDotFill || color, properties.tickmarksStyleDotStroke || color ); } else if ( tickmarks == 'square' || tickmarks == 'rect' || tickmarks == 'filledsquare' || (tickmarks == 'endsquare' && (index == 0 || index == (lineData.length - 1))) || (tickmarks == 'filledendsquare' && (index == 0 || index == (lineData.length - 1))) ) { this.path( 'b r % % % % f % s %', Math.round(xPos - properties.tickmarksSize), Math.round(yPos - properties.tickmarksSize), properties.tickmarksSize * 2, properties.tickmarksSize * 2, 'white', this.context.strokeStyle ); // Fillrect if (tickmarks == 'filledsquare' || tickmarks == 'filledendsquare') { this.path( 'b r % % % % f %', Math.round(xPos - properties.tickmarksSize), Math.round(yPos - properties.tickmarksSize), properties.tickmarksSize * 2, properties.tickmarksSize * 2, isShadow ? properties.shadowColor : this.context.strokeStyle ); } this.path('f null s null'); // // Diamond style tickmarks // } else if ( tickmarks === 'diamond' || tickmarks === 'filleddiamond' || (tickmarks === 'enddiamond' && (index == 0 || index == (lineData.length - 1))) || (tickmarks === 'filledenddiamond' && (index == 0 || index == (lineData.length - 1))) ) { this.path( 'b m % % l % % l % % l % % c f % s', xPos - properties.tickmarksSize, yPos, xPos, yPos - properties.tickmarksSize, xPos + properties.tickmarksSize, yPos, xPos, yPos + properties.tickmarksSize, tickmarks.substr(0, 6) === 'filled' ? (isShadow ? properties.shadowColor : this.context.strokeStyle) : 'white' ); // // Filled arrowhead // } else if (tickmarks == 'filledarrow') { // If the spline option is enabled then update the // variables that are used to calculate the arrow if (properties.spline) { xPos = this.coordsSpline[dataset][this.coordsSpline[dataset].length - 1][0]; yPos = this.coordsSpline[dataset][this.coordsSpline[dataset].length - 1][1]; prevX = this.coordsSpline[dataset][this.coordsSpline[dataset].length - 3][0]; prevY = this.coordsSpline[dataset][this.coordsSpline[dataset].length - 3][1]; } var x = Math.abs(xPos - prevX); var y = Math.abs(yPos - prevY); if (yPos < prevY) { var a = Math.atan(x / y) + 1.57; } else { var a = Math.atan(y / x) + 3.14; } this.path( 'b lj miter m % % a % % % % % false a % % % % % false c s % f %', xPos, yPos, xPos, yPos, properties.tickmarksSize, a - 0.3, a - 0.3, xPos, yPos, properties.tickmarksSize, a + 0.3, a + 0.3, this.context.strokeStyle, this.context.fillStyle ); // // Arrow head, NOT filled // } else if (tickmarks === 'arrow') { // If the spline option is enabled then update the // variables that are used to calculate the arrow if (properties.spline) { xPos = this.coordsSpline[dataset][this.coordsSpline[dataset].length - 1][0]; yPos = this.coordsSpline[dataset][this.coordsSpline[dataset].length - 1][1]; prevX = this.coordsSpline[dataset][this.coordsSpline[dataset].length - 2][0]; prevY = this.coordsSpline[dataset][this.coordsSpline[dataset].length - 2][1]; } var orig_linewidth = this.context.lineWidth; var x = Math.abs(xPos - prevX); var y = Math.abs(yPos - prevY); this.context.lineWidth; if (yPos < prevY) { var a = Math.atan(x / y) + 1.57; } else { var a = Math.atan(y / x) + 3.14; } this.path( 'b lj miter m % % a % % % % % false m % % a % % % % % false s % lw %', xPos, yPos, xPos, yPos, properties.tickmarksSize, a - 0.3, a - 0.3, xPos, yPos, xPos, yPos, properties.tickmarksSize, a + 0.3, a + 0.3, this.context.strokeStyle, orig_linewidth ); // // Image based tickmark // // lineData, xPos, yPos, color, isShadow, prevX, prevY, tickmarks, index } else if ( typeof tickmarks === 'string' && ( tickmarks.substr(0, 6) === 'image:' || tickmarks.substr(0, 5) === 'data:' || tickmarks.substr(0, 1) === '/' || tickmarks.substr(0, 3) === '../' || tickmarks.substr(0, 7) === 'images/' || tickmarks.substr(0, 4) === 'src:' ) ) { var img = new Image(); if (tickmarks.substr(0, 6) === 'image:') { img.src = tickmarks.substr(6); } else if (tickmarks.substr(0, 4) === 'src:') { img.src = tickmarks.substr(4); } else { img.src = tickmarks; } var obj = this; img.onload = function () { if (properties.tickmarksStyleImageHalign === 'center') xPos -= (this.width / 2); if (properties.tickmarksStyleImageHalign === 'right') xPos -= this.width; if (properties.tickmarksStyleImageValign === 'center') yPos -= (this.height / 2); if (properties.tickmarksStyleImageValign === 'bottom') yPos -= this.height; xPos += properties.tickmarksStyleImageOffsetx; yPos += properties.tickmarksStyleImageOffsety; obj.context.drawImage(this, xPos, yPos); }; // // Custom tick drawing function // } else if (typeof tickmarks == 'function') { tickmarks( this, lineData, lineData[index], index, xPos, yPos, color, prevX, prevY ); } }; // // Draws a filled range if necessary // this.drawRange = function () { // // Fill the range if necessary // if (properties.filledRange && properties.filled) { if (RGraph.isNullish(properties.filledRangeThreshold)) { properties.filledRangeThreshold = this.ymin properties.filledRangeThresholdColors = [properties.filledColors, properties.filledColors] } for (var idx=0; idx<2; ++idx) { var threshold_colors = properties.filledRangeThresholdColors; var y = this.getYCoord(properties.filledRangeThreshold) this.context.save(); if (idx == 0) { this.context.beginPath(); this.context.rect(0,0,this.canvas.width,y); this.context.clip(); } else { this.context.beginPath(); this.context.rect(0,y,this.canvas.width, this.canvas.height); this.context.clip(); } this.context.beginPath(); this.context.fillStyle = (idx == 1 ? properties.filledRangeThresholdColors[1] : properties.filledRangeThresholdColors[0]); this.context.lineWidth = !this.hidden(idx) ? 1 : 0; var len = (this.coords.length / 2); for (var i=0; i<len; ++i) { if (!RGraph.isNullish(this.coords[i][1])) { if (i == 0) { this.context.moveTo(this.coords[i][0], this.coords[i][1]) } else { this.context.lineTo(this.coords[i][0], this.coords[i][1]) } } } for (var i=this.coords.length - 1; i>=len; --i) { if (RGraph.isNullish(this.coords[i][1])) { this.context.moveTo(this.coords[i][0], this.coords[i][1]) } else { this.context.lineTo(this.coords[i][0], this.coords[i][1]) } //this.context.lineTo(this.coords[i][0], this.coords[i][1]) } // Taken out - 10th Oct 2012 //this.context.stroke(); this.context.fill(); this.context.restore(); } } }; // // Redraws the line with the correct line width etc // // @param array coords The coordinates of the line // this.redrawLine = function (coords, color, linewidth, index) { if (!properties.redraw || properties.filledRange) { return; } this.context.strokeStyle = (typeof color == 'object' && color && color.toString().indexOf('CanvasGradient') == -1 ? color[0] : color); this.context.lineWidth = linewidth; // Added this on 1/1/17 to facilitate dotted and dashed lines if (properties.dotted || properties.dashed ) { if (properties.dashed) { this.context.setLineDash([2,6]) } else if (properties.dotted) { this.context.setLineDash([1,5]) } } if (this.hidden(index)) { this.context.strokeStyle = 'rgba(0,0,0,0)'; } if (properties.spline) { this.drawCurvyLine(coords, this.hidden(index) ? 'rgba(0,0,0,0)' : color, linewidth, index); return; } this.setLinejoin({index: index}); this.setLinecap({index: index}); this.context.beginPath(); var len = coords.length; var width = this.canvas.width var height = this.canvas.height; var penUp = false; for (var i=0; i<len; ++i) { var xPos = coords[i][0]; var yPos = coords[i][1]; if (i > 0) { var prevX = coords[i - 1][0]; var prevY = coords[i - 1][1]; } if (( (i == 0 && coords[i]) || (yPos < this.marginTop) || (prevY < this.marginTop) || (yPos > (height - this.marginBottom)) || (i > 0 && prevX > (width - this.marginRight)) || (i > 0 && prevY > (height - this.marginBottom)) || prevY == null || penUp == true ) && (!properties.outofbounds || yPos == null || prevY == null) ) { if (RGraph.ISOLD && yPos == null) { // ...? } else { this.context.moveTo(coords[i][0], coords[i][1]); } penUp = false; } else { if (properties.stepped && i > 0) { this.context.lineTo(coords[i][0], coords[i - 1][1]); } // Don't draw the last bit of a stepped chart. Now DO //if (!this.properties.stepped || i < (coords.length - 1)) { this.context.lineTo(coords[i][0], coords[i][1]); //} penUp = false; } } // // If two colors are specified instead of one, go over the up bits // if ( properties.colorsAlternate && typeof color == 'object' && color[0] && color[1]) { for (var i=1; i<len; ++i) { var prevX = coords[i - 1][0]; var prevY = coords[i - 1][1]; if (prevY != null && coords[i][1] != null) { this.context.beginPath(); this.context.strokeStyle = color[coords[i][1] < prevY ? 0 : 1]; this.context.lineWidth = properties.linewidth; this.context.moveTo(prevX, prevY); this.context.lineTo(coords[i][0], coords[i][1]); this.context.stroke(); } } } // Added the stroke and beginPath in on 5/1/19 as dotted/dashed // wasn't working correctly. // this.context.stroke(); this.context.beginPath(); if (properties.dashed || properties.dotted) { //this.context.setLineDash([1,0]); this.context.setLineDash([]); } }; // // Draw the backdrop // this.drawBackdrop = function (coords, color) { var size = properties.backdropSize; this.path( 'lw % ga % ss %', size, properties.backdropAlpha, color ); var yCoords = []; this.context.beginPath(); if (properties.spline) { // The DrawSpline function only takes the Y coords so extract them from the coords that have // (which are X/Y pairs) for (var i=0; i<coords.length; ++i) { yCoords.push(coords[i][1]) } this.drawSpline(this.context, yCoords, color, null); } else { this.context.moveTo(coords[0][0], coords[0][1]); for (var j=1; j<coords.length; ++j) { if ( RGraph.isNullish(coords[j][1]) || (coords[j - 1] && RGraph.isNullish(coords[j-1][1])) ) { this.context.moveTo(coords[j][0], coords[j][1]); } else { this.context.lineTo(coords[j][0], coords[j][1]); } } } this.context.stroke(); // Reset the alpha value this.context.globalAlpha = 1; RGraph.noShadow(this); }; // // Returns the linewidth // this.getLineWidth = function (i) { var linewidth = properties.linewidth; if (typeof linewidth == 'number') { return linewidth; } else if (typeof linewidth === 'object') { if (linewidth[i]) { return linewidth[i]; } else { return linewidth[0]; } alert('[LINE] Error! The linewidth option should be a single number or an array of one or more numbers'); } }; // // The getShape() method - used to get the point the mouse is currently over, if any // // @param object e The event object // @param object OPTIONAL You can pass in the bar object instead of the // function getting it from the canvas // this.getShape = function (e) { var obj = this, mouseXY = RGraph.getMouseXY(e), mouseX = mouseXY[0], mouseY = mouseXY[1]; // This facilitates you being able to pass in the bar object as a parameter instead of // the function getting it from the object if (arguments[1]) { obj = arguments[1]; } for (var i=0; i<obj.coords.length; ++i) { if (RGraph.tooltipsHotspotIgnore(this, i)) { continue; } var x = obj.coords[i][0], y = obj.coords[i][1], dataset = 0, idx = i; while ((idx + 1) > this.data[dataset].length) { idx -= this.data[dataset].length; dataset++; } // Do this if the hotspot is triggered by the X coord AND the Y coord if ( mouseX <= (x + properties.tooltipsHotspotSize) && mouseX >= (x - properties.tooltipsHotspotSize) && mouseY <= (y + properties.tooltipsHotspotSize) && mouseY >= (y - properties.tooltipsHotspotSize) && (this.properties.clip ? RGraph.clipTo.test(this, mouseX, mouseY) : true) ) { if (RGraph.parseTooltipText) { var tooltip = RGraph.parseTooltipText(properties.tooltips, i); } // Don't return points for hidden datasets // Added 10/08/17 // Fixed 22/09/17 Thanks to zsolt - this should be a continue // not a return. if (this.hidden(dataset)) { continue; } return { object: obj, x: x, y: y, dataset: dataset, index: idx, sequentialIndex: i, label: properties.xaxisLabels && typeof properties.xaxisLabels[idx] === 'string' ? properties.xaxisLabels[idx] : null, tooltip: typeof tooltip === 'string' ? tooltip : null }; } else if ( properties.tooltipsHotspotXonly == true && mouseX <= (x + properties.tooltipsHotspotSize) && mouseX >= (x - properties.tooltipsHotspotSize)) { var tooltip = RGraph.parseTooltipText(properties.tooltips, i); return { object: obj, x: x, y: y, dataset: dataset, index: idx, sequentialIndex: i, label: properties.xaxisLabels && typeof properties.xaxisLabels[idx] === 'string' ? properties.xaxisLabels[idx] : null, tooltip: tooltip }; } } }; // // The getShapeByX() method - used to get the point the mouse is currently over, if any // but it ONLY considers the X coordinate - not the Y // // @param object e The event object // @param object OPTIONAL You can pass in the bar object instead of the // function getting it from the canvas // this.getShapeByX = function (e) { var obj = this, mouseXY = RGraph.getMouseXY(e), mouseX = mouseXY[0], mouseY = mouseXY[1]; // This facilitates you being able to pass in the bar object as a parameter instead of // the function getting it from the object if (arguments[1]) { obj = arguments[1]; } for (var i=0; i<obj.coords.length; ++i) { var x = obj.coords[i][0], y = obj.coords[i][1], dataset = 0, idx = i; while ((idx + 1) > this.data[dataset].length) { idx -= this.data[dataset].length; dataset++; } if (mouseX <= (x + properties.tooltipsHotspotSize) && mouseX >= (x - properties.tooltipsHotspotSize)) { return { object: obj, x: x, y: y, dataset: dataset, index: idx, sequentialIndex: i, label: properties.xaxisLabels && typeof properties.xaxisLabels[idx] === 'string' ? properties.xaxisLabels[idx] : null }; } } }; // // Draws the above line labels // this.drawAboveLabels = function () { var units_pre = properties.labelsAboveUnitsPre, units_post = properties.labelsAboveUnitsPost, decimals = properties.labelsAboveDecimals, point = properties.labelsAbovePoint, thousand = properties.labelsAboveThousand, bgcolor = properties.labelsAboveBackground || 'white', border = (( typeof properties.labelsAboveBorder === 'boolean' || typeof properties.labelsAboveBorder === 'number' ) ? properties.labelsAboveBorder : true), offsety = properties.labelsAboveOffsety, specific = properties.labelsAboveSpecific, formatter = properties.labelsAboveFormatter, data_arr = RGraph.arrayLinearize(this.original_data);; var textConf = RGraph.getTextConf({ object: this, prefix: 'labelsAbove' }); offsety -= textConf.size; // Use this to 'reset' the drawing state this.context.beginPath(); // Don't need to check that chart.labels.above is enabled here, it's been done already for (var i=0, len=this.coords.length; i<len; i+=1) { var indexes = RGraph.sequentialIndexToGrouped (i, this.data), dataset = indexes[0], index = indexes[1], coords = this.coords[i]; // Don't draw a label for null values if (RGraph.isNullish(coords[1])) { continue; } if (this.hidden(dataset)) { continue; } RGraph.text({ object: this, font: textConf.font, size: textConf.size, color: textConf.color, bold: textConf.bold, italic: textConf.italic, x: coords[0] + properties.labelsAboveOffsetx, y: coords[1] + offsety, text: (specific && specific[i]) ? specific[i] : (specific ? '' : RGraph.numberFormat({ object: this, number: typeof decimals === 'number' ? data_arr[i].toFixed(decimals) : data_arr[i], value: typeof decimals === 'number' ? data_arr[i].toFixed(decimals) : data_arr[i], unitspre: units_pre, unitspost: units_post, point: point, thousand: thousand, formatter: formatter, index: index, dataset: dataset })), valign: 'center', halign: 'center', bounding: true, boundingFill: bgcolor, boundingStroke: border ? 'black' : 'rgba(0,0,0,0)', tag: 'labels.above' }); } }; // // Draw a curvy line. // this.drawCurvyLine = function (coords, color, linewidth, index) { var yCoords = []; for (var i=0; i<coords.length; ++i) { yCoords.push(coords[i][1]); } if (properties.filled) { this.context.beginPath(); var xaxisY = this.getYCoord( properties.yaxisScaleMin); this.context.moveTo(coords[0][0],xaxisY); this.drawSpline(this.context, yCoords, color, index); if (properties.filledAccumulative && index > 0) { for (var i=(this.coordsSpline[index - 1].length - 1); i>=0; i-=1) { this.context.lineTo(this.coordsSpline[index - 1][i][0], this.coordsSpline[index - 1][i][1]); } } else { this.context.lineTo(coords[coords.length - 1][0],xaxisY); } this.context.fill(); } this.context.beginPath(); this.drawSpline(this.context, yCoords, color, index); this.context.stroke(); }; // // When you click on the chart, this method can return the Y value at that point. It works for any point on the // chart (that is inside the gutters) - not just points on the Line. // // @param object e The event object // this.getValue = function (arg) { if (arg.length == 2) { var mouseX = arg[0]; var mouseY = arg[1]; } else { var mouseCoords = RGraph.getMouseXY(arg); var mouseX = mouseCoords[0]; var mouseY = mouseCoords[1]; } var obj = this; var xaxispos = properties.xaxisPosition; if (mouseY < properties.marginTop) { return xaxispos == 'bottom' || xaxispos == 'center' ? this.max : this.min; } else if (mouseY > (this.canvas.height - properties.marginBottom)) { return xaxispos == 'bottom' ? this.min : this.max; } if ( properties.xaxisPosition == 'center') { var value = (( (obj.grapharea / 2) - (mouseY - properties.marginTop)) / obj.grapharea) * (obj.max - obj.min); value *= 2; value > 0 ? value += this.min : value -= this.min; return value; } else if ( properties.xaxisPosition == 'top') { var value = ((obj.grapharea - (mouseY - properties.marginTop)) / obj.grapharea) * (obj.max - obj.min); value = Math.abs(obj.max - value) * -1; return value; } else { var value = ((obj.grapharea - (mouseY - properties.marginTop)) / obj.grapharea) * (obj.max - obj.min) value += obj.min; return value; } }; // // Each object type has its own Highlight() function which highlights the appropriate shape // // @param object shape The shape to highlight // this.highlight = function (shape) { if (properties.tooltipsHighlight) { if (typeof properties.highlightStyle === 'function') { (properties.highlightStyle)(shape); // Inverted highlighting } else if (properties.highlightStyle === 'invert') { // Clip to the graph area this.path( 'sa b r % % % % cl', properties.marginLeft, properties.marginTop, this.canvas.width - properties.marginLeft - properties.marginRight, this.canvas.height - properties.marginTop - properties.marginBottom ); this.path( 'b m % % a % % 25 4.71 4.72 true l % % l % % l % % l % % l % % c f %', shape.x, properties.marginTop, shape.x, shape.y, shape.x, properties.marginTop, this.canvas.width - properties.marginRight, properties.marginTop, this.canvas.width - properties.marginRight, this.canvas.height - properties.marginBottom, properties.marginLeft, this.canvas.height - properties.marginBottom, properties.marginLeft, properties.marginTop, properties.highlightFill ); // Draw a border around the circular cutout this.path( 'b a % % 25 0 6.29 false s % rs', shape.x, shape.y, properties.highlightStroke ); // Halo style highlighting } else if (properties.highlightStyle === 'halo') { var obj = shape.object, color = properties.colors[shape.dataset]; // Clear a space in white first for the tickmark obj.path( 'b a % % 13 0 6.2830 false f rgba(255,255,255,0.75)', shape.x, shape.y ); obj.path( 'ga 0.15 b a % % 13 0 6.2830 false f % ga 1', shape.x, shape.y, color ); obj.path( 'b a % % 7 0 6.2830 false f white', shape.x, shape.y ); obj.path( 'b a % % 5 0 6.2830 false f %', shape.x, shape.y, color ); } else { this.context.lineWidth = 1; RGraph.Highlight.point(this, shape); } } }; // // The getObjectByXY() worker method. Don't call this call: // // RGraph.ObjectRegistry.getObjectByXY(e) // // @param object e The event object // this.getObjectByXY = function (e) { var mouseXY = RGraph.getMouseXY(e); // The 5 is so that the cursor doesn't have to be over the graphArea to trigger the hotspot if ( (mouseXY[0] > properties.marginLeft - 5) && mouseXY[0] < (this.canvas.width - properties.marginRight + 5) && mouseXY[1] > ( properties.marginTop - 5) && mouseXY[1] < (this.canvas.height - properties.marginBottom + 5) ) { return this; } }; // // This method handles the adjusting calculation for when the mouse is moved // // @param object e The event object // this.adjusting_mousemove = function (e) { // // Handle adjusting for the Bar // if (properties.adjustable && RGraph.Registry.get('adjusting') && RGraph.Registry.get('adjusting').uid == this.uid) { // Rounding the value to the given number of decimals make the chart step var value = Number(this.getValue(e)); var shape = RGraph.Registry.get('adjusting.shape'); if (shape) { RGraph.Registry.set('adjusting.shape', shape); this.original_data[shape.dataset][shape.index] = Number(value); RGraph.redrawCanvas(e.target); RGraph.fireCustomEvent(this, 'onadjust'); } } }; // // This function can be used when the canvas is clicked on (or similar - depending on the event) // 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) { if (arguments[1] === true) { var allowOutOfBounds = true; } if (typeof value != 'number') { return null; } var y; var xaxispos = properties.xaxisPosition; if (xaxispos == 'top') { // Account for negative numbers //if (value < 0) { // value = Math.abs(value); //} y = ((value - this.min) / (this.max - this.min)) * this.grapharea; // Inverted Y labels if ( properties.yaxisScaleInvert) { y = this.grapharea - y; } y = y + this.marginTop } else if (xaxispos == 'center') { y = ((value - this.min) / (this.max - this.min)) * (this.grapharea / 2); y = (this.grapharea / 2) - y; y += this.marginTop; } else { if (!allowOutOfBounds && ((value < this.min || value > this.max) && properties.outofbounds == false) ) { return null; } y = ((value - this.min) / (this.max - this.min)) * this.grapharea; // Inverted Y labels if ( properties.yaxisScaleInvert) { y = this.grapharea - y; } y = this.canvas.height - this.marginBottom - y; } return y; }; // // This function draws a curvy line // // @param object context The 2D context // @param array coords The coordinates // this.drawSpline = function (context, coords, color, index) { this.coordsSpline[index] = []; var xCoords = []; var marginLeft = properties.marginLeft; var marginRight = properties.marginRight; var hmargin = properties.marginInner; var interval = (this.canvas.width - (marginLeft + marginRight) - (2 * hmargin)) / (coords.length - 1); this.context.strokeStyle = color; // // 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 - 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); this.context.lineTo(xCoords[xCoords.length - 1], yCoord); if (typeof index == 'number') { this.coordsSpline[index].push( [xCoords[xCoords.length - 1], yCoord] ); } } } // Draw the last section this.context.lineTo(((j-1) * interval) + marginLeft + hmargin, P[j]); if (typeof index == 'number') { this.coordsSpline[index].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))); } }; // // This allows for easy specification of gradients // this.parseColors = function () { // This is necessary for some reason //var properties = this.properties; // Save the original colors so that they can be // restored when the canvas is reset if (this.original_colors.length === 0) { this.original_colors.colors = RGraph.arrayClone(properties.colors); this.original_colors.fillledColors = RGraph.arrayClone(properties.filledColors); this.original_colors.keyColors = RGraph.arrayClone(properties.keyColors); this.original_colors.backgroundHbars = RGraph.arrayClone(properties.backgroundHbars); this.original_colors.backgroundBarsColor1 = properties.backgroundBarsColor1; this.original_colors.backgroundBarsColor2 = properties.backgroundBarsColor2; this.original_colors.backgroundGridColor = properties.backgroundGridColor; this.original_colors.backgroundColor = properties.backgroundColor; this.original_colors.textColor = properties.textColor; this.original_colors.crosshairsColor = properties.crosshairsColor; this.original_colors.annotatableColor = properties.annotatableColor; this.original_colors.titleColor = properties.titleColor; this.original_colors.xaxisTitleColor = properties.xaxisTitleColor; this.original_colors.yaxisTitleColor = properties.yaxisTitleColor; this.original_colors.keyBackground = properties.keyBackground; this.original_colors.axesColor = properties.axesColor; this.original_colors.highlightFill = properties.highlightFill; } for (var i=0; i<properties.colors.length; ++i) { if (typeof properties.colors[i] == 'object' && properties.colors[i][0] && properties.colors[i][1]) { properties.colors[i][0] = this.parseSingleColorForGradient( properties.colors[i][0]); properties.colors[i][1] = this.parseSingleColorForGradient( properties.colors[i][1]); } else { properties.colors[i] = this.parseSingleColorForGradient( properties.colors[i]); } } // // Filled.colors // if (properties.filledColors) { if (typeof properties.filledColors == 'string') { properties.filledColors = this.parseSingleColorForGradient(properties.filledColors, 'vertical'); } else { for (var i=0; i<properties.filledColors.length; ++i) { properties.filledColors[i] = this.parseSingleColorForGradient(properties.filledColors[i], 'vertical'); } } } // // Key colors // if (!RGraph.isNullish(properties.keyColors)) { for (var i=0; i<properties.keyColors.length; ++i) { properties.keyColors[i] = this.parseSingleColorForGradient(properties.keyColors[i]); } } // // Background horizontal bars colors // if (!RGraph.isNullish(properties.backgroundHbars)) { for (var i=0; i<properties.backgroundHbars.length; ++i) { properties.backgroundHbars[i][2] = this.parseSingleColorForGradient(properties.backgroundHbars[i][2]); } } // // Parse various properties for colors // var props = [ 'backgroundBarsColor1', 'backgroundBarsColor2', 'backgroundGridColor', 'backgroundColor', 'crosshairsColor', 'annotatableColor', 'textColor', 'titleColor', 'xaxisTitleColor', 'yaxisTitleColor', 'keyBackground', 'axesColor', 'highlightFill' ]; for (var i=0; i<props.length; ++i) { properties[props[i]] = this.parseSingleColorForGradient(properties[props[i]]); } }; // // Use this function to reset the object to the post-constructor state. Eg reset colors if // need be etc // this.reset = function () { }; // // This parses a single color value // this.parseSingleColorForGradient = function (color) { if (!color || typeof color != 'string') { return color; } // // Horizontal or vertical gradient // var dir = typeof arguments[1] == 'string' ? arguments[1] : 'vertical'; if (typeof color === 'string' && color.match(/^gradient\((.*)\)$/i)) { // Allow for JSON gradients if (color.match(/^gradient\(({.*})\)$/i)) { return RGraph.parseJSONGradient({object: this, def: RegExp.$1}); } var parts = RegExp.$1.split(':'); // Create the gradient if (dir == 'horizontal') { var grad = this.context.createLinearGradient(0,0,this.canvas.width,0); } else { var grad = this.context.createLinearGradient(0,this.canvas.height - properties.marginBottom,0, properties.marginTop); } var diff = 1 / (parts.length - 1); grad.addColorStop(0, RGraph.trim(parts[0])); for (var j=1; j<parts.length; ++j) { grad.addColorStop( j * diff, RGraph.trim(parts[j]) ); } } return grad ? grad : color; }; // // Sets the appropriate shadow // this.setShadow = function (i) { if (properties.shadow) { // // Handle the appropriate shadow color. This now facilitates an array of differing // shadow colors // var shadowColor = properties.shadowColor; // // Accommodate an array of shadow colors as well as a single string // if (typeof shadowColor == 'object' && shadowColor[i - 1]) { this.context.shadowColor = shadowColor[i]; } else if (typeof shadowColor == 'object') { this.context.shadowColor = shadowColor[0]; } else if (typeof shadowColor == 'string') { this.context.shadowColor = shadowColor; } this.context.shadowBlur = properties.shadowBlur; this.context.shadowOffsetX = properties.shadowOffsetx; this.context.shadowOffsetY = properties.shadowOffsety; } }; // // This function handles highlighting an entire data-series for the interactive // key // // @param int index The index of the data series to be highlighted // this.interactiveKeyHighlight = function (index) { var coords = this.coords2[index]; if (coords) { var pre_linewidth = this.context.lineWidth; var pre_linecap = this.context.lineCap; this.context.lineWidth = properties.linewidth + 10; this.context.lineCap = 'round'; this.context.strokeStyle = properties.keyInteractiveHighlightChartStroke; this.context.beginPath(); if (properties.spline) { this.drawSpline(this.context, coords, properties.keyInteractiveHighlightChart, null); } else { for (var i=0,len=coords.length; i<len; i+=1) { if ( i == 0 || RGraph.isNullish(coords[i][1]) || (typeof coords[i - 1][1] != undefined && RGraph.isNullish(coords[i - 1][1]))) { this.context.moveTo(coords[i][0], coords[i][1]); } else { this.context.lineTo(coords[i][0], coords[i][1]); } } } this.context.stroke(); // Reset the lineCap and lineWidth this.context.lineWidth = pre_linewidth; this.context.lineCap = pre_linecap; } }; // // 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; } if (typeof this[type] !== 'function') { this[type] = func; } else { RGraph.addCustomEventListener(this, type, func); } return this; }; // // This function runs once only // (put at the end of the file (before any effects)) // this.firstDrawFunc = function () { }; // // Draws error-bars for the Bar and Line charts // this.drawErrorbars = function () { // Save the state of the canvas so that it can be restored at the end this.context.save(); RGraph.noShadow(this); var coords = this.coords, x = 0, errorbars = properties.errorbars, length = 0; // If not capped set the width of the cap to zero if (!properties.errorbarsCapped) { properties.errorbarsCappedWidth = 0.001; halfwidth = 0.0005; } // Set the linewidth this.context.lineWidth = properties.errorbarsLinewidth; for (var i=0; i<coords.length; ++i) { var halfwidth = properties.errorbarsCappedWidth / 2 || 5, color = properties.errorbarsColor || 'black'; // Set the perbar linewidth if the fourth option in the array // is specified if (errorbars[i] && typeof errorbars[i][3] === 'number') { this.context.lineWidth = errorbars[i][3]; } else if (typeof properties.errorbarsLinewidth === 'number') { this.context.lineWidth = properties.errorbarsLinewidth; } else { this.context.lineWidth = 1; } // Calulate the pixel size if (typeof errorbars === 'number' || typeof errorbars[i] === 'number') { if (typeof errorbars === 'number') { var positiveLength = this.getYCoord(this.min) - this.getYCoord(this.min + errorbars), negativeLength = positiveLength; } else { var positiveLength = this.getYCoord(this.min) - this.getYCoord(this.min + errorbars[i]), negativeLength = positiveLength; } if (positiveLength || negativeLength) { this.path( 'lj miter lc square b m % % l % % m % % l % % l % % m % % l % % s %', coords[i][0] - halfwidth,coords[i][1] + negativeLength, coords[i][0] + halfwidth,coords[i][1] + negativeLength, coords[i][0],coords[i][1] + negativeLength, coords[i][0],coords[i][1] - positiveLength, coords[i][0] - halfwidth,coords[i][1] - positiveLength, coords[i][0],coords[i][1] - positiveLength, coords[i][0] + halfwidth,coords[i][1] - positiveLength, color ); this.path( 'lj miter lc square b m % % l % % s %', coords[i][0] - halfwidth,coords[i][1] + negativeLength, coords[i][0] + halfwidth,coords[i][1] + negativeLength, color ); } } else if (typeof errorbars[i] === 'object' && !RGraph.isNullish(errorbars[i])) { var positiveLength = this.getYCoord(this.min) - this.getYCoord(this.min + errorbars[i][0]), negativeLength = this.getYCoord(this.min) - this.getYCoord(this.min + errorbars[i][1]); // Color if (typeof errorbars[i][2] === 'string') { color = errorbars[i][2]; } // Cap width halfwidth = typeof errorbars[i][4] === 'number' ? errorbars[i][4] / 2 : halfwidth; // Set the linewidth if (typeof errorbars[i] === 'object' && typeof errorbars[i][3] === 'number') { this.context.lineWidth = errorbars[i][3]; } else if (typeof properties.errorbarsLinewidth === 'number') { this.context.lineWidth = properties.errorbarsLinewidth; } else { this.context.lineWidth = 1; } if (!RGraph.isNullish(errorbars[i][0])) { this.path( 'lc square b m % % l % % l % % m % % l % % s %', coords[i][0],coords[i][1], coords[i][0],coords[i][1] - positiveLength, coords[i][0] - halfwidth,Math.round(coords[i][1] - positiveLength), coords[i][0],Math.round(coords[i][1] - positiveLength), coords[i][0] + halfwidth,Math.round(coords[i][1] - positiveLength), color ); } if (typeof errorbars[i][1] === 'number') { var negativeLength = Math.abs(this.getYCoord(errorbars[i][1]) - this.getYCoord(0)); this.path( 'b m % % l % % l % % m % % l % % s %', coords[i][0],coords[i][1], coords[i][0],coords[i][1] + negativeLength, coords[i][0] - halfwidth,Math.round(coords[i][1] + negativeLength), coords[i][0],Math.round(coords[i][1] + negativeLength), coords[i][0] + halfwidth,Math.round(coords[i][1] + negativeLength), color ); } } } this.context.restore(); }; // // Hides a line by setting the appropriate flag so that the .visible(index) // function returns the relevant result. // // @param int index The index of the line to hide // this.hide = function () { // Hide a single line if (typeof arguments[0] === 'number') { properties.lineVisible[arguments[0]] = false; // Hide multiple lines } else if (typeof arguments[0] === 'object') { for (var i=0; i<arguments[0].length; ++i) { properties.lineVisible[arguments[0][i]] = false; } // Hide all lines } else { for (var i=0; i<this.original_data.length; ++i) { properties.lineVisible[i] = false; } } RGraph.redraw(); // Facilitate chaining return this; }; // // Shows a line by setting the appropriate flag so that the .visible(index) // function returns the relevant result. // // @param int index The index of the line to show // this.show = function () { // Show a single line if (typeof arguments[0] === 'number') { properties.lineVisible[arguments[0]] = true; // Show multiple lines } else if (typeof arguments[0] === 'object') { for (var i=0; i<arguments[0].length; ++i) { properties.lineVisible[arguments[0][i]] = true; } // Show all lines } else { for (var i=0; i<this.original_data.length; ++i) { properties.lineVisible[i] = true; } } RGraph.redraw(); // Facilitate chaining return this; }; // // Returns true/false as to wether a line is hidden or not // // @param int index The index of the line to hide // this.hidden = function (index) { return !properties.lineVisible[index]; }; // // Line chart grow effect // // This effect gradually increases the magnitude of the // points on the Line chart // // @param object Options for the effect // @param function An optional callback that is run when // the effect is finished // this.grow = function () { // Cancel any stop request if one is pending this.cancelStopAnimation(); var obj = this, callback = arguments[1] ? arguments[1] : function () {}, opt = arguments[0] ? arguments[0] : {}, frames = opt.frames ? opt.frames : 30, frame = 0, data = RGraph.arrayClone(this.unmodified_data); this.draw(); var max = this.scale2.max; this.set('yaxisScaleMax', max); RGraph.clear(this.canvas); function iterator () { if (obj.stopAnimationRequested) { // Reset the flag obj.stopAnimationRequested = false; return; } for (var i=0,len=data.length; i<len; ++i) { for (var j=0,len2=data[i].length; j<len2; ++j) { obj.original_data[i][j] = (frame / frames) * data[i][j]; } } RGraph.clear(obj.canvas); RGraph.redrawCanvas(obj.canvas); if (frame < frames) { frame++; RGraph.Effects.updateCanvas(iterator); } else { callback(obj); } } iterator(); return this; }; // // Unfold animation effect // // This effect gradually increases the X/Y coordinates // from 0 // this.unfold = function () { // Cancel any stop request if one is pending this.cancelStopAnimation(); var obj = this, opt = arguments[0] ? arguments[0] : {}, frames = opt.frames ? opt.frames : 30, frame = 0, callback = arguments[1] ? arguments[1] : function () {}, initial = properties.animationUnfoldInitial; properties.animationFactor = properties.animationUnfoldInitial; function iterator () { if (obj.stopAnimationRequested) { // Reset the flag obj.stopAnimationRequested = false; return; } properties.animationFactor = ((1 - initial) * (frame / frames)) + initial; RGraph.clear(obj.canvas); RGraph.redrawCanvas(obj.canvas); if (frame < frames) { frame++; RGraph.Effects.updateCanvas(iterator); } else { callback(obj); } } iterator(); return this; }; // // Trace // // This is a new version of the Trace effect which no longer requires jQuery and is more compatible // with other effects (eg Expand). This new effect is considerably simpler and less code. // // @param object Options for the effect. Currently only "frames" is available. // @param int A function that is called when the ffect is complete // this.trace = function () { // Cancel any stop request if one is pending this.cancelStopAnimation(); var obj = this, opt = arguments[0] || {}, frames = opt.frames || 30, frame = 0, callback = arguments[1] || function () {}; obj.set('animationTraceClip', opt.reverse ? 1 : 0); // Disable the labelsAbove option if (obj.properties.labelsAbove) { obj.set('labelsAbove', false); var enableLabelsAbove = true; } function iterator () { if (obj.stopAnimationRequested) { // Reset the flag obj.stopAnimationRequested = false; return; } RGraph.clear(obj.canvas); RGraph.redrawCanvas(obj.canvas); if (frame++ < frames) { obj.set('animationTraceClip', opt.reverse ? (1 - (frame / frames)) : (frame / frames)); RGraph.Effects.updateCanvas(iterator); } else { if (enableLabelsAbove) { setTimeout(function () { obj.set('labelsAbove', true); RGraph.redraw(); }, 500); } callback(obj); } } iterator(); return this; }; // // A wave effect - like the Bar chart Wave effect // this.wave = function () { // Cancel any stop request if one is pending this.cancelStopAnimation(); // Reset the data to the original this.data = RGraph.arrayClone(this.original_data); this.draw(); // If there's only one point call the grow function instead if (this.original_data[0].length === 1) { this.unfold(arguments[0]); return; } var obj = this, opt = arguments[0] || {}, labelsAbove = this.get('labelsAbove'); opt.frames = opt.frames || 60; opt.startFrames = []; opt.counters = []; var framesperpoint = opt.frames / 3, frame = -1, callback = arguments[1] || function () {}, original = RGraph.arrayClone(this.unmodified_data); // // turn off the labelsAbove option whilst animating // this.set('labelsAbove', false); for (var dataset=0; dataset<original.length; ++dataset) { for (var i=0; i<original[dataset].length; ++i) { opt.startFrames[i] = ((opt.frames / 2) / (original[0].length - 1)) * i; opt.counters[i] = 0; if (!opt.reverse) { this.original_data[dataset][i] = 0; } } } if (opt.reverse) { opt.startFrames = RGraph.arrayReverse(opt.startFrames); } // // This stops the chart from jumping // this.set('yaxisScaleMax', this.scale2.max); RGraph.clear(this.canvas); function iterator () { if (obj.stopAnimationRequested) { // Reset the flag obj.stopAnimationRequested = false; return; } ++frame; // Loop thru each dataset for (var dataset=0; dataset<original.length; ++dataset) { //Loop thru the data in reverse direction if (opt.reverse) { // Loop thru all of the points in each dataset for (var i=(original[dataset].length - 1); i>=0; i-=1) { if (frame > opt.startFrames[i]) { obj.original_data[dataset][i] = Math.max( 0, original[dataset][i] - (Math.abs(original[dataset][i] * ( (opt.counters[i]++) / framesperpoint))) ); // Make the number negative if the original was if (original[dataset][i] < 0) { obj.original_data[dataset][i] *= -1; } } } } else { for (var i=0,len=original[dataset].length; i<len; i+=1) { if (frame > opt.startFrames[i]) { obj.original_data[dataset][i] = Math.min( Math.abs(original[dataset][i]), Math.abs(original[dataset][i] * ( (opt.counters[i]++) / framesperpoint)) ); // Make the number negative if the original was if (original[dataset][i] < 0) { obj.original_data[dataset][i] *= -1; } } } } } if (frame >= opt.frames) { if (labelsAbove) { setTimeout(function () { obj.set('labelsAbove', true); RGraph.redraw(); }, 500); } callback(obj); } else { RGraph.redrawCanvas(obj.canvas); RGraph.Effects.updateCanvas(iterator); } } iterator(); return this; }; // // FoldToCenter // // Line chart FoldTocenter // // @param object OPTIONAL An object map of options // @param function OPTIONAL A callback to run when the effect is complete // this.foldtocenter = function () { // Cancel any stop request if one is pending this.cancelStopAnimation(); var obj = this, opt = arguments[0] || {}, frames = opt.frames || 30, frame = 0, callback = arguments[1] || function () {}, center_value = obj.scale2.max / 2; obj.set('yaxisScaleMax', obj.scale2.max); var original_data = RGraph.arrayClone(obj.original_data); function iterator () { if (obj.stopAnimationRequested) { // Reset the flag obj.stopAnimationRequested = false; return; } for (var i=0,len=obj.original_data.length; i<len; ++i) { if (obj.original_data[i].length) { for (var j=0,len2=obj.original_data[i].length; j<len2; ++j) { var dataset = obj.original_data[i]; if (dataset[j] > center_value) { dataset[j] = original_data[i][j] - ((original_data[i][j] - center_value) * (frame / frames)); } else { dataset[j] = original_data[i][j] + (((center_value - original_data[i][j]) / frames) * frame); } } } } RGraph.clear(obj.canvas); RGraph.redrawCanvas(obj.canvas) if (frame++ < frames) { RGraph.Effects.updateCanvas(iterator); } else { callback(obj); } } iterator(); return this; }; // // UnfoldFromCenterTrace effect // // @param object An object containing options // @param function A callback function // this.unfoldfromcentertrace = this.unfoldFromCenterTrace = function () { // Cancel any stop request if one is pending //this.cancelStopAnimation(); var obj = this, opt = arguments[0] || {}, frames = opt.frames || 30, frame = 0, data = RGraph.arrayClone(obj.original_data), callback = arguments[1] || function () {}; // Draw the chart once to get the scale values obj.canvas.style.visibility = 'hidden'; obj.draw(); var max = obj.scale2.max; RGraph.clear(obj.canvas); obj.canvas.style.visibility = 'visible'; // // When the Trace function finishes it calls this // function // var unfoldCallback = function () { obj.original_data = data; obj.unfoldFromCenter({frames: frames / 2}, callback); }; // // Determine the mid-point // var half = obj.get('xaxisPosition') == 'center' ? obj.min : ((obj.max - obj.min) / 2) + obj.min; obj.set('yaxisScaleMax', obj.max); for (var i=0,len=obj.original_data.length; i<len; ++i) { for (var j=0; j<obj.original_data[i].length; ++j) { obj.original_data[i][j] = (obj.get('filled') && obj.get('filledAccumulative') && i > 0) ? 0 : half; } } RGraph.clear(obj.canvas); obj.trace({frames: frames / 2}, unfoldCallback); return obj; }; // // UnfoldFromCenter // // Line chart unfold from center // // @param object An option map of properties. Only frames is supported: {frames: 30} // @param function An optional callback // this.unfoldfromcenter = this.unfoldFromCenter = function () { // Cancel any stop request if one is pending this.cancelStopAnimation(); var obj = this, opt = arguments[0] || {}, frames = opt.frames || 30, frame = 0, callback = arguments[1] || function () {}; // Draw the chart once to get the scale values obj.canvas.style.visibility = 'hidden'; obj.draw(); var max = obj.scale2.max; RGraph.clear(obj.canvas); obj.canvas.style.visibility = 'visible'; var center_value = obj.get('xaxisPosition') === 'center' ? properties.yaxisScaleMin : ((obj.max - obj.min) / 2) + obj.min, original_data = RGraph.arrayClone(obj.original_data), steps = null; obj.set('yaxisScaleMax', max); if (!steps) { steps = []; for (var dataset=0,len=original_data.length; dataset<len; ++dataset) { steps[dataset] = [] for (var i=0,len2=original_data[dataset].length; i<len2; ++i) { if (properties.filled && properties.filledAccumulative && dataset > 0) { steps[dataset][i] = original_data[dataset][i] / frames; obj.original_data[dataset][i] = center_value; } else { steps[dataset][i] = (original_data[dataset][i] - center_value) / frames; obj.original_data[dataset][i] = center_value; } } } } function iterator () { if (obj.stopAnimationRequested) { // Reset the flag obj.stopAnimationRequested = false; return; } for (var dataset=0; dataset<original_data.length; ++dataset) { for (var i=0; i<original_data[dataset].length; ++i) { obj.original_data[dataset][i] += steps[dataset][i]; } } RGraph.clear(obj.canvas); RGraph.redrawCanvas(obj.canvas); if (--frames > 0) { RGraph.Effects.updateCanvas(iterator); } else { obj.original_data = RGraph.arrayClone(original_data); RGraph.clear(obj.canvas); RGraph.redrawCanvas(obj.canvas); callback(obj); } } iterator(); return this; }; // // Couple of functions that allow you to control the // animation effect // this.stopAnimation = function () { // Reset the data on the chart object to the // unmodified_data variable this.original_data = RGraph.arrayClone(this.unmodified_data); // This, effectively, resets the clip area that // is set up by the trace effect (if the trace // effect is stopped part way through the // animationTraceClip option will be less than // 1 - affecting future draws. // if (this.get('animationTraceClip') !== 1) { this.set('animationTraceClip', 1); } this.stopAnimationRequested = true; }; this.cancelStopAnimation = function () { this.stopAnimationRequested = false; }; // // Determines whether a point is adjustable or not. // // @param object A shape object // this.isAdjustable = function (shape) { if (RGraph.isNullish(properties.adjustableOnly)) { return true; } if (RGraph.isArray(properties.adjustableOnly) && properties.adjustableOnly[shape.sequentialIndex]) { return true; } return false; }; // // A worker function that handles Bar chart specific tooltip substitutions // this.tooltipSubstitutions = function (opt) { var indexes = RGraph.sequentialIndexToGrouped(opt.index, this.data); // //// Dataset tooltips //// if (properties.tooltipsDataset) { return { dataset: indexes[1], index: indexes[0], sequentialIndex: opt.index, values: this.data[indexes[1]] }; // // Regular tooltips // } else { // Create the values array which contains each datasets value for (var i=0,values=[]; i<this.original_data.length; ++i) { values.push(this.original_data[i][indexes[1]]); } return { index: indexes[1], dataset: indexes[0], sequentialIndex: opt.index, value: this.data_arr[opt.index], values: values }; } }; // // A worker function that returns the correct color/label/value // // @param object specific The indexes that are applicable // @param number index The appropriate index // this.tooltipsFormattedCustom = function (specific, index) { return { }; }; // // Adds dataset tooltips // this.addDatasetTooltip = function () { var obj = this; // // This is the function that handles dataset tooltips // this.datasetTooltipsListener = function (e) { var [mx, my] = RGraph.getMouseXY(e); // This defaults the tooltipsDatasetEvent property to mousemove if (!obj.properties.tooltipsDatasetEvent) { obj.properties.tooltipsDatasetEvent = 'click'; } // Now test it var result = obj.over(mx, my) if (RGraph.isObject(result) && RGraph.isNumber(result.dataset)) { var over = true, dataset = result.dataset; // Show the tooltip if we're in the click handler or change the // pointer if we're in the mousemove listener. if ( e.type === 'click' || (e.type === 'mousemove' && obj.properties.tooltipsDatasetEvent === 'mousemove' && (!RGraph.Registry.get('tooltip') || i != RGraph.Registry.get('tooltip').__dataset__)) ) { RGraph.hideTooltip(); RGraph.redraw(); // Set the tooltip positioning obj.set('tooltipsPositionStatic', false); obj.set('tooltipsEffect', 'fade'); if (obj.get('tooltipsDatasetEvent') === 'mousemove') { obj.set('tooltipsEffect', 'none'); } // Add the dataset index to the object obj.tooltipsDatasetIndex = dataset; RGraph.tooltip({ object: obj, text: typeof obj.properties.tooltipsDataset === 'string' ? obj.properties.tooltipsDataset : obj.properties.tooltipsDataset[dataset], x: 0, y: 0, index: dataset, event: e }); // // Position the tooltip // var coords_length = obj.coords2[dataset].length, x = obj.coords2[dataset][Math.floor(coords_length / 2)][0], y = obj.coords2[dataset][Math.floor(coords_length / 2)][1], [cx, cy] = RGraph.getCanvasXY(obj.canvas), tooltip = RGraph.Registry.get('tooltip'), width = tooltip.offsetWidth, height = tooltip.offsetHeight; tooltip.style.left = (cx + x - (width / 2)) + 'px'; tooltip.style.top = (cy + y - height - 20) + 'px'; // // Highlight the dataset // obj.properties.highlightDatasetStrokeAlpha = 0.25; // Set the linecap and linejoin to match // the chart object obj.context.lineCap = obj.properties.linecap; obj.context.lineJoin = obj.properties.linejoin; obj.highlightDataset({ dataset: dataset, linewidth: obj.getLineWidth(dataset) + 10, stroke: obj.properties.filled ? 'transparent' : obj.properties.colors[dataset] }); obj.properties.highlightDatasetStrokeAlpha = 1; } if (e.type === 'mousemove') { e.target.style.cursor = 'pointer'; } } if (!over) { // Hide the tooltip if the event is click if (e.type === 'click') { RGraph.hideTooltip(); RGraph.redraw(); } // Reset the cursor type obj.canvas.style.cursor = 'default'; } e.stopPropagation(); }; if (!this.datasetTooltipsListenerAdded) { this.canvas.addEventListener('click', this.datasetTooltipsListener, false); this.canvas.addEventListener('mousemove', this.datasetTooltipsListener, false); window.addEventListener('click', function (e) { RGraph.redraw(); }, false); this.datasetTooltipsListenerAdded = true; } }; // // This allows for static tooltip positioning // this.positionTooltipStatic = function (args) { var obj = args.object, e = args.event, tooltip = args.tooltip, index = args.index, canvasXY = RGraph.getCanvasXY(obj.canvas) coords = this.coords[args.index]; // Position the tooltip in the X direction args.tooltip.style.left = ( canvasXY[0] // The X coordinate of the canvas + coords[0] // The X coordinate of the point on the chart - (tooltip.offsetWidth / 2) // Subtract half of the tooltip width + obj.properties.tooltipsOffsetx // Add any user defined offset ) + 'px'; args.tooltip.style.top = ( canvasXY[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 + obj.properties.tooltipsOffsety // Add any user defined offset ) + '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 () { var args = RGraph.getArgs(arguments, 'dataset'); var obj = this; var color = properties.trendlineColor; var linewidth = properties.trendlineLinewidth; var margin = properties.trendlineMargin; // If clipping is enabled then draw a clip box the same as the graph area // (just the chart area, not including the margins) if (properties.trendlineClip) { this.path( 'b sa r % % % % cl', properties.marginLeft, properties.marginTop, this.canvas.width - properties.marginLeft - properties.marginRight, this.canvas.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.isArray(properties.trendlineColors)) { color = properties.trendlineColors; } // handle the options being arrays if (typeof color === 'object' && color[args.dataset]) { color = color[args.dataset]; } else if (typeof color === 'object') { color = 'gray'; } if (typeof linewidth === 'object' && typeof linewidth[args.dataset] === 'number') { linewidth = linewidth[args.dataset]; } else if (typeof linewidth === 'object') { linewidth = 1; } if (typeof margin === 'object' && typeof margin[args.dataset] === 'number') { margin = margin[args.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[args.dataset].length; ++i) { totalX += data[args.dataset][i][0]; totalY += data[args.dataset][i][1]; } var averageX = totalX / data[args.dataset].length; var averageY = totalY / data[args.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[args.dataset].length; ++i) { xCoordMinusAverageX[i] = data[args.dataset][i][0] - averageX; yCoordMinusAverageY[i] = data[args.dataset][i][1] - averageY; // b. Multiply the averages valuesMultiplied[i] = xCoordMinusAverageX[i] * yCoordMinusAverageY[i]; xCoordMinusAverageSquared[i] = xCoordMinusAverageX[i] * xCoordMinusAverageX[i]; } var sumOfValuesMultiplied = RGraph.arraySum(valuesMultiplied); var sumOfXCoordMinusAverageSquared = RGraph.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] = this.marginLeft; coords[0][1] = this.getYCoord(coords[0][1], true); coords[1][0] = this.canvas.width - this.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.isArray(properties.trendlineDashed) && properties.trendlineDashed[args.dataset]) ) { this.context.setLineDash([4,4]); } if ( properties.trendlineDotted === true || (RGraph.isArray(properties.trendlineDotted) && properties.trendlineDotted[args.dataset])) { this.context.setLineDash([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.isArray(properties.trendlineDashArray)) { if ( properties.trendlineDashArray.length === 2 && typeof properties.trendlineDashArray[0] === 'number' && typeof properties.trendlineDashArray[1] === 'number' ) { this.context.setLineDash(properties.trendlineDashArray); } else if ( RGraph.isArray(properties.trendlineDashArray) && RGraph.isArray(properties.trendlineDashArray[args.dataset])) { this.context.setLineDash(properties.trendlineDashArray[args.dataset]); } } // These variables hold the coordinates var x1, x2, y1, y2; // Draw the line this.path( ' lc round lw % b m % % l % % s %', linewidth, // moveTo x1 = Math.max(coords[0][0], this.coords2[args.dataset][0][0] - margin), y1 = coords[0][1], // lineTo x2 = Math.min(coords[1][0], this.coords2[args.dataset][this.coords2[args.dataset].length - 1][0] + margin), y2 = coords[1][1], // stroke color color ); // Store the trendline coordinates this.coordsTrendline[args.dataset] = [ [x1, y1], [x2, y2] ]; // Reset the line dash array this.context.setLineDash([5,0]); // // Reset the clipping region // if (properties.trendlineClip) { this.context.restore(); } }; // // 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 gray 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.isNumber(data[i])) { readData = true; } if (RGraph.isNullish(data[i]) && readData) { start = i - 1; for (var j=(i+1); j<data.length; ++j) { if (RGraph.isNullish(data[j])) { continue; } else { end = j; } this.context.setLineDash(properties.nullBridgeDashArray); this.path( 'b lw % m % % l % % s %', typeof properties.nullBridgeLinewidth === 'number' ? properties.nullBridgeLinewidth : this.getLineWidth(datasetIdx), this.coords2[datasetIdx][start][0], this.coords2[datasetIdx][start][1], this.coords2[datasetIdx][end][0], this.coords2[datasetIdx][end][1], typeof properties.nullBridgeColors === 'string' ? properties.nullBridgeColors : ((typeof properties.nullBridgeColors === 'object' && !RGraph.isNullish(properties.nullBridgeColors) && properties.nullBridgeColors[datasetIdx]) ? properties.nullBridgeColors[datasetIdx] : properties.colors[datasetIdx]) ); start = null; end = null; break; } } } }; // // Draws the angled labels that follow // the line up and down. Bit similar to // labelsAbove, but they're positioned // above the line and not the points // on the line. // this.drawAngledLabels = function () { if (properties.labelsAngled) { // Turn off any lingering shadow RGraph.noShadow(this); // This is the first lines coordinates var coords = this.coords; // This function gets the relevant text // configuration from all (12) of the text // configuration properties var getTextConfiguration = function (dir) { // Init the textConf object var textConf = {}; // Get the up text configuration var prefixes = ['text', 'labelsAngled', 'labelsAngled' + dir]; var textProperties = ['Font','Color','Size','Bold','Italic']; for (var prefix in prefixes) { for (var prop in textProperties) { var name = prefixes[prefix] + textProperties[prop]; if (name) { if ( RGraph.isString(properties[name]) || RGraph.isNumber(properties[name]) || RGraph.isBoolean(properties[name]) ) { textConf[textProperties[prop].toLowerCase()] = properties[name]; } } } } return textConf; }; // Loop thru the coordinates for the line (but not // the last one) for (var i=0; i<(coords.length) - 1; ++i) { // Work out the horizontal and vertical distance to the next point var dx = (coords[i + 1][0] - coords[i][0]) / 2, dy = (coords[i + 1][1] - coords[i][1]) / 2; // Work out the direction that the line is going so // that the correct label can be used if (coords[i + 1][1] < coords[i][1]) { var direction = 0; // Up var textConf = getTextConfiguration('Up'); } else if (coords[i + 1][1] > coords[i][1]) { var direction = 1; // Down var textConf = getTextConfiguration('Down'); } else { var direction = 2; // Level var textConf = getTextConfiguration('Level'); } // Work out the angle that the text should be drawn at var angle = RGraph.getAngleByXY({ cx: coords[i][0], cy: coords[i][1], x: coords[i + 1][0], y: coords[i + 1][1], }); // Use the API function to add the text to the chart RGraph.text({ object: this, accessible: properties.adjustable ? false : (RGraph.isBoolean(properties.labelsAngledAccessible) ? properties.labelsAngledAccessible : true), font: textConf.font, color: textConf.color, size: textConf.size, bold: textConf.bold, italic: textConf.italic, x: coords[i][0] + dx, y: coords[i][1] + dy - 5, text: (properties.labelsAngledSpecific && (RGraph.isString(properties.labelsAngledSpecific[i]) || RGraph.isNumber(properties.labelsAngledSpecific[i]))) ? properties.labelsAngledSpecific[i] : properties.labelsAngled[direction], halign: 'center', valign: 'bottom', angle: angle * (180 / Math.PI) }); } } }; // // Sets the linecap style // Not always very noticeable, but these do have an effect // with thick lines // // butt square round // this.setLinecap = function () { var args = RGraph.getArgs(arguments, 'index'); if (RGraph.isArray(properties.linecap) && RGraph.isString(properties.linecap[args.index])) { this.context.lineCap = properties.linecap[args.index]; } else if ( RGraph.isString(properties.linecap) ) { this.context.lineCap = properties.linecap; } else { this.context.lineCap = 'round'; } }; // // Finds the closest point to the given mouseX coordinate. It allows a // tolerance of 10 (or so) pixels. // // @param object opt An object consisting of; // o coords The coordinates of the points // o mousex The mouseX coordinate // o tolerance The number of pixels leeway // that is allowed. Default is 10 // this.closest = function (opt) { var DEFAULT_TOLERANCE = 25, ret = []; // custom object given if (typeof opt === 'object' && typeof opt.event === 'object' && !opt.event.type) { if (typeof opt.tolerance !== 'number') { opt.tolerance = DEFAULT_TOLERANCE; } // Event object } else if (typeof opt === 'object' && typeof opt.pageX === 'number') { var e = opt; opt = {event: e, tolerance: DEFAULT_TOLERANCE}; } var coords = this.coords2, mouseXY = RGraph.getMouseXY(opt.event), tolerance = (typeof opt.tolerance === 'number' ? opt.tolerance : DEFAULT_TOLERANCE); // Loop through the coordinates looking for the closest // (going by X coordinate) for (var dataset=0; dataset<coords.length; ++dataset) { for (var index=0; index<coords[dataset].length; ++index) { // // Only go by the x coordinate // if (opt.xonly) { if ( mouseXY[0] > (coords[dataset][index][0] - tolerance) && mouseXY[0] < (coords[dataset][index][0] + tolerance)) { ret.push({dataset: dataset, index: index, distance: Math.abs(mouseXY[0] - coords[dataset][index][0])}); } // // Go by both the X and Y coordinates // } else { var hyp = RGraph.getHypLength({ x1: coords[dataset][index][0], y1: coords[dataset][index][1], x2: mouseXY[0], y2: mouseXY[1] }); if (hyp <= tolerance) { ret.push({dataset: dataset, index: index,distance: hyp}); } } // End else clause } // End for loop }// End for loop // // Sort the ret array in order of the distance // ret.sort(function (a, b) { return a.distance - b.distance; }); // Return the point closest to the click return ret[0]; }; // // The animation function that makes the point grow to // a new position. Give it the index, the new value and // optionally the dataset too (this defaults to zero). // // @param number dataset The dataset of the point OPTIONAL // @param number index The index of the point // @param number value The value to grow the point to // @param number frames The number of frames to use whilst // animating to the new position OPTIONAL // this.growPoint = function () { var args = RGraph.getArgs(arguments, 'index,value'), obj = this; // args.dataset should default to zero if not given if (typeof args.dataset !== 'number') { args.dataset = 0; } // Determine the original value or the point that's being adjusted var original_value = this.original_data[args.dataset][args.index]; var frames = typeof args.frames === 'number' ? args.frames : 15, delay = 16.666; for (var i=0; i<frames; i++) { (function (i) { setTimeout(function () { obj.original_data[args.dataset][args.index] = ((args.value - original_value) * (i + 1) / frames) + original_value; // Update this so that the above labels are correctly updated obj.data_arr = RGraph.arrayLinearize(obj.original_data); RGraph.redraw(); }, delay * i); })(i) } }; // // Sets the linejoin style // // round miter bevel // this.setLinejoin = function () { var args = RGraph.getArgs(arguments, 'index'); if (RGraph.isArray(properties.linejoin) && RGraph.isString(properties.linejoin[args.index])) { this.context.lineJoin = properties.linejoin[args.index]; } else if ( RGraph.isString(properties.linejoin) ) { this.context.lineJoin = properties.linejoin; } else { this.context.lineJoin = 'round'; } }; // // This returns the relevant value for the formatted key // macro %{value}. THIS VALUE SHOULD NOT BE FORMATTED. // // @param number index The index in the dataset to get // the value for // this.getKeyValue = function (index) { if ( RGraph.isArray(this.properties.keyFormattedValueSpecific) && RGraph.isNumber(this.properties.keyFormattedValueSpecific[index])) { return this.properties.keyFormattedValueSpecific[index]; } else { return RGraph.arraySum(this.data[index]); } }; // // Returns how many data-points there should be when a string // based key property has been specified. For example, this: // // key: '%{property:_labels[%{index}]} %{value_formatted}' // // ...depending on how many bits of data ther is might get // turned into this: // // key: [ // '%{property:_labels[%{index}]} %{value_formatted}', // '%{property:_labels[%{index}]} %{value_formatted}', // '%{property:_labels[%{index}]} %{value_formatted}', // '%{property:_labels[%{index}]} %{value_formatted}', // '%{property:_labels[%{index}]} %{value_formatted}', // ] // // ... ie in that case there would be 4 data-points so the // template is repeated 4 times. // this.getKeyNumDatapoints = function () { return this.data.length; }; // // This function tests the given coords to see if // they're over the line. It draws the path using // the coordinates from the line chart (either // the regular coords or the spline coordinates) // and then uses the isPointInstroke() function. // // @param number x The X coordinate to test // @param number y The Y coordinate to test // @return boolean Whether the given coordinates // are over the line or not. // this.over = function (x, y) { var args = RGraph.getArgs(arguments, 'x,y'); var datasets = properties.spline ? this.coordsSpline : this.coords2; // Loop through the set of coords for each line for (var i=0; i<datasets.length; ++i) { // Current datasets coordinates var dataset = i; // Move to the first points coordinates this.path( 'b lw % lc round ss rgba(0,0,0,0) m % %', this.properties.linewidth + 5, datasets[dataset][0][0], datasets[dataset][0][1] ); // Loop through the coords for the line for (var j=1; j<datasets[dataset].length; ++j) { this.context.lineTo( datasets[dataset][j][0], datasets[dataset][j][1] ); } // This is the shape object that is retuned // with details of any line/fill that is // currently being hovered over. More // information is added when it can be var shape = { object: this, dataset: dataset }; // // Accommodate stacked filled line charts // if ( this.properties.filled && this.properties.filledAccumulative ) { // Highlight the first dataset // Draw a line to the X axis and back // to the first point if (dataset === 0) { this.context.lineTo( datasets[dataset][datasets[dataset].length - 1][0], this.canvas.height - this.properties.marginBottom ); this.context.lineTo( this.properties.marginLeft, this.canvas.height - this.properties.marginBottom ); // Other datasets // // Have to stroke the line over the // coordinates and then backtrack // over the previous lines coordinates } else if (dataset > 0) { // Draw a line to the top of the previous dataset and // back to the first point var prevDatasetIdx = dataset - 1; // Draw a line to the bottom of the canvas this.context.lineTo( datasets[dataset][datasets[dataset].length - 1][0], this.canvas.height - this.properties.marginBottom ); // Draw a line to the LHS of the canvas this.context.lineTo( this.properties.marginLeft, this.canvas.height - this.properties.marginBottom ); this.context.closePath(); shape.dataset = dataset; } // Accommodate non-stacked, filled charts } else if ( this.properties.filled && !this.properties.filledAccumulative ) { // Reverse the dataset index as the // datasets need to be checked in reverse // order dataset = datasets.length - dataset - 1; this.context.beginPath(); // Loop through the coordinates // of the shape for (var k=0; k<datasets[dataset].length; ++k) { k === 0 ? this.context.moveTo(datasets[dataset][k][0], datasets[dataset][k][1]) : this.context.lineTo(datasets[dataset][k][0], datasets[dataset][k][1]); } // Draw a line to the bottom of the chart this.context.lineTo( datasets[dataset][datasets[dataset].length - 1][0], this.canvas.height - this.properties.marginBottom ); // Draw a line back to the LHS of the chart this.context.lineTo( datasets[dataset][0][0], this.canvas.height - this.properties.marginBottom ); this.context.closePath(); this.context.fillStyle = 'rgba(0,0,0,0.5)'; // Add filled shape details to the // shape (if any) shape.dataset = dataset; } //this.context.stroke(); // Return the index of the line if the mouse // pointer is over it it's in the stroke if ( this.context.isPointInStroke(args.x, args.y) || (this.properties.filled && this.context.isPointInPath(args.x, args.y)) ) { // This shape object is created up above // and the necessary bits are added to it // as they're determined return shape; } } return null; }; // // Used to highlight a particular dataset. This // function accommodates fill, non-filled, splines // and non-spline charts. // // @param opt object A little object that should // contain configuration // information: // // linewidth: The linewidth // stroke: The color of the // stroke // fill: The color of the fill // this.highlightDataset = function () { var args = RGraph.getArgs(arguments, 'opt'); var coords = this.properties.spline ? this.coordsSpline : this.coords2; // Default to the first dataset args.opt.dataset = parseInt(args.opt.dataset) || 0; // Default this to an empty object if (!args.opt) args.opt = {}; if (!args.opt.stroke) args.opt.stroke = this.properties.highlightDatasetStroke; if (!args.opt.fill) args.opt.fill = this.properties.highlightDatasetFill; if (!args.opt.dotted) args.opt.fotted = this.properties.highlightDatasetDotted; if (!args.opt.dashed) args.opt.dashed = this.properties.highlightDatasetDashed; if (!args.opt.linedash) args.opt.linedash = this.properties.highlightDatasetDashArray; if (!RGraph.isNumber(args.opt.linewidth)) args.opt.linewidth = this.properties.linewidth; // // If the colors are arrays - deal with that if (RGraph.isArray(args.opt.stroke) && args.opt.stroke[args.opt.dataset]) { args.opt.stroke = args.opt.stroke[args.opt.dataset]; } if (RGraph.isArray(args.opt.fill) && args.opt.fill[args.opt.dataset]) { args.opt.fill = args.opt.fill[args.opt.dataset]; } // Start the path this.context.beginPath(); // Set the colors and linewidth this.context.strokeStyle = args.opt.stroke; this.context.fillStyle = args.opt.fill; this.context.lineWidth = args.opt.linewidth; // If the highlighDatasetStrokeUseColors property is set // then set the strokeStyle to the relevant // obj.properties.colors color if (this.properties.highlightDatasetStrokeUseColors && this.properties.colors[args.opt.dataset]) { this.context.strokeStyle = this.properties.colors[args.opt.dataset]; } // If the highlighDatasetFillUseColors property is set // then set the fillStyle to the relevant // obj.properties.colors color if (this.properties.highlightDatasetFillUseColors && this.properties.colors[args.opt.dataset]) { this.context.fillStyle = this.properties.colors[args.opt.dataset]; } // If the highlighDatasetFillUseColors property is set AND the // obj.properties.filledColors property is being used // then set the fillStyle to the relevant // obj.properties.filledColors color if (this.properties.highlightDatasetFillUseColors && RGraph.isArray(this.properties.filledColors) && this.properties.filledColors[args.opt.dataset]) { this.context.fillStyle = this.properties.filledColors[args.opt.dataset]; } // Fotted or dashed settings if (args.opt.dotted || args.opt.dashed) { if (args.opt.dashed) { this.context.setLineDash([2,6]) } else if (args.opt.dotted) { this.context.setLineDash([1,5]) } } else if (args.opt.linedash) { this.context.setLineDash(args.opt.linedash); } if (args.opt.linewidth === 0) { this.context.strokeStyle = 'transparent'; } // Loop through the coordinates // of the shape for (var i=0; i<coords[args.opt.dataset].length; ++i) { i === 0 ? this.context.moveTo(coords[args.opt.dataset][i][0], coords[args.opt.dataset][i][1]) : this.context.lineTo(coords[args.opt.dataset][i][0], coords[args.opt.dataset][i][1]); } // // If this is a stacked & filled chart then we // need to add the previous datasets coordinates // to the path // if ( this.properties.filled && this.properties.filledAccumulative && args.opt.dataset > 0 ) { // Necessary? this.context.lineTo( coords[args.opt.dataset - 1][coords[args.opt.dataset - 1].length - 1][0], coords[args.opt.dataset - 1][coords[args.opt.dataset - 1].length - 1][1] ); for (let i=(coords[args.opt.dataset - 1].length - 1); i>=0; --i) { this.context.lineTo( coords[args.opt.dataset - 1][i][0], coords[args.opt.dataset - 1][i][1] ); } } // First dataset or non-stacked charts - // draw a line down to the bottom of // the chart. // // *** ONLY DO THIS FOR FILLED CHARTS *** // if (this.properties.filled && (args.opt.dataset === 0 || !this.properties.filledAccumulative) ) { if (args.opt.dataset === 0 || !this.properties.filledAccumulative) { this.context.lineTo( coords[args.opt.dataset][coords[args.opt.dataset].length - 1][0], this.canvas.height - this.properties.marginBottom ); } this.context.lineTo( this.properties.marginLeft, this.canvas.height - this.properties.marginBottom ); } // Close the path // // *** ONLY DO THIS FOR FILLED CHARTS *** // if (this.properties.filled) { this.context.closePath(); // Enable the highlightDatasetFillAlpha if (RGraph.isNumber(this.properties.highlightDatasetFillAlpha)) { this.context.globalAlpha = this.properties.highlightDatasetFillAlpha; } // Fill the shape - but only on filled // charts this.context.fill(); this.context.globalAlpha = 1; } // Enable the highlightDatasetStrokeAlpha if (RGraph.isNumber(this.properties.highlightDatasetStrokeAlpha)) { this.context.globalAlpha = this.properties.highlightDatasetStrokeAlpha; } // Stroke the shape this.context.stroke(); this.context.globalAlpha = 1; }; // // 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 in the // RGraph.clipTo.start() function. // // @param string clip The clip string as supplied by the // user in the chart configuration // this.clipToScaleWorker = function (clip) { // The Regular expression is actually done by the // calling RGraph.clipTo.start() function in the core // library if (RegExp.$1 === 'min') from = this.scale2.min; else from = Number(RegExp.$1); if (RegExp.$2 === 'max') to = this.scale2.max; else to = Number(RegExp.$2); var width = this.canvas.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; } this.path( 'sa b r % % % % cl', x, y, width, height ); }; // // This function handles clipping to scale values TESTING. // Because each chart handles scales differently, a worker // function is needed instead of it all being done // centrally in the RGraph.clipTo.start() function. // // @param string clip The clip string as supplied by the // user in the chart configuration // this.clipToScaleTestWorker = function (clip) { // The Regular expression is actually done by the // calling RGraph.clipTo.start() function in the core // library if (RegExp.$1 === 'min') from = this.scale2.min; else from = Number(RegExp.$1); if (RegExp.$2 === 'max') to = this.scale2.max; else to = Number(RegExp.$2); var width = this.canvas.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; } this.path( 'b r % % % %', x, y, width, height ); }; // // Register the object so it is redrawn when necessary // RGraph.register(this); // // Allow all lines to start off as visible // for (var i=0; i<this.original_data.length; ++i) { properties.lineVisible[i] = true; } // // This is the 'end' of the constructor so if the first argument // contains configuration data - handle that. // RGraph.parseObjectStyleConfig(this, conf.options); }; // // 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.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.Line({ id: args.id, data: RGraph.arrayClone(args.data, true), options: RGraph.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.canvas.width,coords[1][1]], [obj1.canvas.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.canvas.width,coords[1][1]], [obj1.canvas.width, obj1.canvas.height], [0,obj1.canvas.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.isObject(args.optionsTop)) { for (i in args.optionsTop) { if (RGraph.isString(i)) { obj1.set(i, args.optionsTop[i]); } } } // // Create a new chart that's clipped to the bottom part // coordinates. // var obj2 = new RGraph.Line({ id: args.id, data: RGraph.arrayClone(args.data, true), options: { ...RGraph.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.isObject(args.optionsBottom)) { for (i in args.optionsBottom) { if (RGraph.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.isString(args.animationEffect) && obj1[args.animationEffect] && obj1[args.animationEffect] ) { var effect = args.animationEffect; var effectOptions = args.animationEffectOptions ? args.animationEffectOptions : null; var effectCallback = function () { RGraph.runOnce('rgraph-canvas-line-dual-color-effect-callback', function () { args.animationEffectCallback ? args.animationEffectCallback() : function () {}; }); } obj1[effect](effectOptions); obj2[effect](effectOptions, effectCallback); } else { RGraph.redraw(); } return [obj1, obj2]; };