// 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 scatter graph constructor // RGraph.Scatter = function (conf) { this.data = new Array(conf.data.length); // Store the data set(s) this.data = RGraph.arrayClone(conf.data, true); // Convert objects to arrays for (let i=0; i<this.data.length; ++i) { // Single dataset if ( RGraph.isObject(conf.data[i]) && (RGraph.isNumber(this.data[i].x) || RGraph.isString(this.data[i].x)) && (RGraph.isNumber(this.data[i].y) || RGraph.isString(this.data[i].y))) { conf.data[i] = [ conf.data[i].x, conf.data[i].y, conf.data[i].color || undefined, typeof conf.data[i].tooltip === 'string' ? conf.data[i].tooltip : undefined ]; // Multiple datasets } else if (RGraph.isArray(conf.data[i])) { for (let j=0; j<conf.data[i].length; ++j) { if (RGraph.isObject(conf.data[i][j])) { conf.data[i][j] = [ conf.data[i][j].x, conf.data[i][j].y, conf.data[i][j].color || undefined, typeof conf.data[i][j].tooltip === 'string' ? conf.data[i][j].tooltip : undefined ]; } } } } // Account for just one dataset being given if (typeof conf.data === 'object' && typeof conf.data[0] === 'object' && (typeof conf.data[0][0] === 'number' || typeof conf.data[0][0] === 'string')) { var tmp = RGraph.arrayClone(conf.data, true); conf.data = new Array(); conf.data[0] = RGraph.arrayClone(tmp, true); } this.data = RGraph.arrayClone(conf.data, true); // // Create the sequential indexes map arrays // this.dataIndexMapGroupedToSequential = []; this.dataIndexMapSequentialToGrouped = []; for (let dataset=0,seq=0; dataset<this.data.length; ++dataset) { for (let index=0; index<this.data[dataset].length; ++index,++seq) { // Ensure the arrays are initialised if (!RGraph.isArray(this.dataIndexMapGroupedToSequential[dataset])) { this.dataIndexMapGroupedToSequential[dataset] = []; } if (!RGraph.isArray(this.dataIndexMapGroupedToSequential[dataset][index])) { this.dataIndexMapGroupedToSequential[dataset][index] = []; } this.dataIndexMapGroupedToSequential[dataset][index] = seq; this.dataIndexMapSequentialToGrouped[seq] = [dataset, index]; } } // // End of creating the sequential indexes map arrays // // If necessary convert X/Y values passed as strings // to numbers for (var i=0,len=this.data.length; i<len; ++i) { // Datasets for (var j=0,len2=this.data[i].length; j<len2; ++j) { // Points // Handle the conversion of X values if (typeof this.data[i][j] === 'object' && !RGraph.isNullish(this.data[i][j]) && typeof this.data[i][j][0] === 'string') { if (this.data[i][j][0].match(/^[.0-9]+$/)) { this.data[i][j][0] = parseFloat(this.data[i][j][0]); } else if (this.data[i][j][0] === '') { this.data[i][j][0] = 0; } } // Handle the conversion of Y values if (typeof this.data[i][j] === 'object' && !RGraph.isNullish(this.data[i][j]) && typeof this.data[i][j][1] === 'string') { if (this.data[i][j][1].match(/[.0-9]+/)) { this.data[i][j][1] = parseFloat(this.data[i][j][1]); } else if (this.data[i][j][1] === '') { this.data[i][j][1] = 0; } } } } // Store a copy of the data that won't be touched this.unmodified_data = RGraph.arrayClone(this.data, true); this.id = conf.id; this.canvas = document.getElementById(this.id); this.canvas.__object__ = this; this.context = this.canvas.getContext ? this.canvas.getContext('2d') : null; this.max = 0; this.type = 'scatter'; 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.coords = []; this.coordsText = []; this.coordsBubble = []; this.coordsTrendline = []; this.original_colors = []; this.firstDraw = true; // After the first draw this will be false this.stopAnimationRequested = false;// Used to control the animations // Various config properties this.properties = { backgroundBarsCount: null, backgroundBarsColor1: 'rgba(0,0,0,0)', backgroundBarsColor2: 'rgba(0,0,0,0)', backgroundHbars: null, backgroundVbars: null, backgroundGrid: true, backgroundGridLinewidth: 1, backgroundGridColor: '#ddd', backgroundGridHsize: 20, backgroundGridVsize: 20, backgroundGridVlines: true, backgroundGridHlines: true, backgroundGridBorder: true, backgroundGridAutofit: true, backgroundGridAutofitAlign: true, backgroundGridHlinesCount: 5, backgroundGridVlinesCount: 20, backgroundGridDashed: false, backgroundGridDotted: false, backgroundGridDashArray: 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, colors: [], // This is used internally for the tooltip key colorsBubbleGraduated: true, colorsBubbleStroke: null, textColor: 'black', textFont: 'Arial, Verdana, sans-serif', textSize: 12, textBold: false, textItalic: false, textAccessible: false, textAccessibleOverflow: 'visible', textAccessiblePointerevents:false, text: null, tooltips: [], // Default must be an empty array tooltipsEffect: 'slide', tooltipsEvent: 'onmousemove', tooltipsHotspot: 3, tooltipsCssClass: 'RGraph_tooltip', tooltipsCss: null, 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, tooltipsPointer: true, tooltipsPointerOffsetx: 0, tooltipsPointerOffsety: 0, tooltipsPositionStatic: true, tooltipsHotspotIgnore: 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: '', xaxisLabelsCount: null, xaxisLabelsSize: null, xaxisLabelsFont: null, xaxisLabelsItalic: null, xaxisLabelsBold: null, xaxisLabelsColor: null, xaxisLabelsOffsetx: 0, xaxisLabelsOffsety: 0, xaxisLabelsHalign: null, xaxisLabelsValign: null, xaxisLabelsPosition: 'section', xaxisLabelsSpecificAlign: 'left', 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', xaxisScale: false, xaxisScaleMin: 0, xaxisScaleMax: null, xaxisScaleUnitsPre: '', xaxisScaleUnitsPost: '', xaxisScaleLabelsCount: 10, xaxisScaleFormatter: null, xaxisScaleDecimals: 0, xaxisScaleThousand: ',', xaxisScalePoint: '.', 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, yaxisScaleInvert: false, yaxisScaleFormatter: null, 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, tickmarksStyle: 'cross', tickmarksStyleImageHalign: 'center', tickmarksStyleImageValign: 'center', tickmarksStyleImageOffsetx: 0, tickmarksStyleImageOffsety: 0, tickmarksSize: 5, marginLeft: 35, marginRight: 35, marginTop: 35, marginBottom: 35, title: '', titleBold: null, titleItalic: null, titleFont: null, titleSize: 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, labelsIngraph: null, labelsIngraphFont: null, labelsIngraphSize: null, labelsIngraphColor: null, labelsIngraphBold: null, labelsIngraphItalic: null, labelsIngraphOffsetx: 0, labelsIngraphOffsety: 0, labelsAbove: false, labelsAboveSize: null, labelsAboveFont: null, labelsAboveColor: null, labelsAboveBold: null, labelsAboveItalic: null, labelsAboveDecimals: 0, labelsAboveOffsetx: 0, labelsAboveOffsety: 0, contextmenu: null, colorsDefault: 'black', crosshairs: false, crosshairsHline: true, crosshairsVline: true, crosshairsColor: '#333', crosshairsLinewidth: 1, crosshairsCoords: false, crosshairsCoordsFixed: true, crosshairsCoordsLabelsX: 'X', crosshairsCoordsLabelsY: 'Y', crosshairsCoordsFormatterX: null, crosshairsCoordsFormatterY: null, crosshairsSnapToScale: false, annotatable: false, annotatableColor: 'black', annotatableLinewidth: 1, lasso: false, lassoFill: '#0064', lassoStroke: '#006', lassoLinewidth: 1, lassoHighlightLinewidth: 1, lassoHighlightStroke: 'transparent', lassoHighlightFill: 'red', lassoCallback: null, lassoClearCallback: null, lassoPersist: false, lassoPersistLocal: true, lassoPersistLoad: null, lassoPersistSave: null, line: false, lineLinewidth: 1, lineColors: ['green', 'red','blue','orange','pink','brown','black','gray'], lineShadowColor: 'rgba(0,0,0,0)', lineShadowBlur: 2, lineShadowOffsetx: 3, lineShadowOffsety: 3, lineStepped: false, lineVisible: true, key: null, keyBackground: 'white', keyPosition: 'graph', keyHalign: 'right', keyShadow: false, keyShadowColor: '#666', keyShadowBlur: 3, keyShadowOffsetx: 2, keyShadowOffsety: 2, keyPositionGutterBoxed: false, keyPositionX: null, keyPositionY: null, keyInteractive: false, keyInteractiveHighlightChartFill: 'rgba(255,0,0,0.9)', keyInteractiveHighlightLabel: 'rgba(255,0,0,0.2)', keyColorShape: 'square', keyRounded: true, keyLinewidth: 1, keyColors: null, keyLabelsColor: null, keyLabelsFont: null, keyLabelsSize: null, keyLabelsBold: null, keyLabelsItalic: null, keyLabelsOffsetx: 0, keyLabelsOffsety: 0, keyFormattedDecimals: 0, keyFormattedPoint: '.', keyFormattedThousand: ',', keyFormattedUnitsPre: '', keyFormattedUnitsPost: '', keyFormattedValueSpecific: null, keyFormattedItemsCount: null, boxplotWidth: 10, boxplotCapped: true, trendline: false, trendlineColors: ['gray'], trendlineLinewidth: 1, trendlineMargin: 15, trendlineDashed: true, trendlineDotted: false, trendlineDashArray: null, trendlineClipping: null, highlightStroke: 'rgba(0,0,0,0)', highlightFill: 'rgba(255,255,255,0.7)', bubbleMin: 0, bubbleMax: null, bubbleWidth: null, bubbleData: null, bubbleLinewidth: 1, bubbleShadow: false, bubbleShadowColor: '#aaa', bubbleShadowOffsetx: 2, bubbleShadowOffsety: 2, bubbleShadowBlur: 3, marimekkoLinewidth: 10, marimekkoColors: ['#faa', '#afa', '#aaf', '#ffa', '#faf', '#aff'], marimekkoColorsSequential: false, marimekkoColorsStroke: 'white', marimekkoLabels: null, marimekkoLabelsColor: null, marimekkoLabelsSize: null, marimekkoLabelsFont: null, marimekkoLabelsBold: null, marimekkoLabelsItalic: null, marimekkoLabelsOffsetx: 0, marimekkoLabelsOffsety: 0, marimekkoLabelsFormattedDecimals: 0, marimekkoLabelsFormattedPoint: '.', marimekkoLabelsFormattedThousand: ',', marimekkoLabelsFormattedUnitsPre: '', marimekkoLabelsFormattedUnitsPost:'', marimekkoLabelsIngraph: false, marimekkoLabelsIngraphColor: null, marimekkoLabelsIngraphSize: 10, marimekkoLabelsIngraphFont: null, marimekkoLabelsIngraphBold: null, marimekkoLabelsIngraphItalic: null, marimekkoLabelsIngraphUnitsPre: '', marimekkoLabelsIngraphUnitsPost: '', marimekkoLabelsIngraphPoint: '.', marimekkoLabelsIngraphThousand: ',', marimekkoLabelsIngraphDecimals: 0, marimekkoLabelsIngraphOffsetx: 0, marimekkoLabelsIngraphOffsety: 0, marimekkoLabelsIngraphBackgroundFill: '#fffa', marimekkoLabelsIngraphBackgroundStroke: 'transparent', marimekkoLabelsIngraphSpecific: null, adjustable: false, adjustableOnly: null, clearto: 'rgba(0,0,0,0)', outofbounds: false, animationTrace: false, animationTraceClip: 1, horizontalLines: null } // // 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; } } // // This allows the data points to be given as dates as well as numbers. Formats supported by RGraph.parseDate() are accepted. // // ALSO: unrelated but this loop is also used to convert null values to an // empty array // for (var i=0; i<this.data.length; ++i) { for (var j=0; j<this.data[i].length; ++j) { // Convert null data points to an empty erray if ( RGraph.isNullish(this.data[i][j]) ) { this.data[i][j] = []; } // Allow for the X point to be dates if (this.data[i][j] && typeof this.data[i][j][0] == 'string') { this.data[i][j][0] = RGraph.parseDate(this.data[i][j][0]); } } } // // Now make the data_arr array - all the data as one big // array // this.data_arr = []; for (var i=0; i<this.data.length; ++i) { for (var j=0; j<this.data[i].length; ++j) { this.data_arr.push(this.data[i][j]); } } // Create the $ objects so that they can be used for (var i=0; i<this.data_arr.length; ++i) { this['$' + i] = {} } // Check for support if (!this.canvas) { alert('[SCATTER] No canvas support'); return; } // 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; // // A simple setter // // @param string name The name of the property to set // @param string value The value of the property // 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; } // Reset the colorsParsed flag if required if ( name === 'backgroundVbars' || name === 'backgroundHbars' || name === 'lineColors' || name === 'colorsDefault' || name === 'crosshairsColor' || name === 'highlightStroke' || name === 'highlightFill' || name === 'backgroundBarsColor1' || name === 'backgroundBarsColor2' || name === 'backgroundGridColor' || name === 'backgroundColor' || name === 'axesColor' || name === 'marimekkoColors' || name === 'marimekkoColorsStrokeroke' || name === 'marimekkoLabelsIngraphBackgroundStroke' || name === 'marimekkoLabelsIngraphBackgroundFill') { 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; // If a single tooltip has been given add it to each datapiece if (name === 'tooltips' && typeof value === 'string') { this.populateTooltips(); } return this; }; // // A simple getter // // @param string name The name of the property to set // 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 Scatter 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; } // // Stop this growing uncontrollably // this.coordsText = []; // // Populate the labels string/array if its a string // if (properties.xaxisLabels && properties.xaxisLabels.length) { // // If the labels option is a string then turn it // into an array. // if (typeof properties.xaxisLabels === 'string') { properties.xaxisLabels = RGraph.arrayPad({ array: [], length: properties.xaxisLabelsCount, value: properties.xaxisLabels }); } for (var i=0; i<properties.xaxisLabels.length; ++i) { if (typeof properties.xaxisLabels[i] === 'object' && properties.xaxisLabels[i].length === 2) { var label = properties.xaxisLabels[i][0]; } else { var label = properties.xaxisLabels[i]; } label = RGraph.labelSubstitution({ object: this, text: label, index: i, value: this.data[0][i], decimals: properties.xaxisLabelsFormattedDecimals || 0, unitsPre: properties.xaxisLabelsFormattedUnitsPre || '', unitsPost: properties.xaxisLabelsFormattedUnitsPost || '', thousand: properties.xaxisLabelsFormattedThousand || ',', point: properties.xaxisLabelsFormattedPoint || '.' }); if (typeof properties.xaxisLabels[i] === 'object' && properties.xaxisLabels[i].length === 2) { properties.xaxisLabels[i][0] = label; } else { properties.xaxisLabels[i] = label; } } } // // Make the margins easy ro access // this.marginLeft = properties.marginLeft; this.marginRight = properties.marginRight; this.marginTop = properties.marginTop; this.marginBottom = properties.marginBottom; // Go through all the data points and see if a tooltip has been given this.hasTooltips = false; var overHotspot = false; // Reset the coords array this.coords = []; // // This facilitates the xmax, xmin and X values being dates // if (typeof properties.xaxisScaleMin == 'string') properties.xaxisScaleMin = RGraph.parseDate(properties.xaxisScaleMin); if (typeof properties.xaxisScaleMax == 'string') properties.xaxisScaleMax = RGraph.parseDate(properties.xaxisScaleMax); // // Look for tooltips and populate the tooltips // // NB 26/01/2011 Updated so that the tooltips property is ALWAYS populated // //if (!RGraph.ISOLD) { this.set('tooltips', []); for (var i=0,len=this.data.length; i<len; i+=1) { for (var j =0,len2=this.data[i].length;j<len2; j+=1) { if (this.data[i][j] && this.data[i][j][3]) { properties.tooltips.push(this.data[i][j][3]); this.hasTooltips = true; } else { properties.tooltips.push(null); } } } //} // Reset the maximum value this.max = 0; // Work out the maximum Y value //if (properties.ymax && properties.ymax > 0) { if (typeof properties.yaxisScaleMax === 'number') { this.max = properties.yaxisScaleMax; this.min = properties.yaxisScaleMin ? properties.yaxisScaleMin : 0; this.scale2 = RGraph.getScale({object: this, options: { 'scale.max': this.max, 'scale.min': this.min, '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.min = this.scale2.min; var decimals = properties.yaxisScaleDecimals; } else { var i = 0; var j = 0; for (i=0,len=this.data.length; i<len; i+=1) { for (j=0,len2=this.data[i].length; j<len2; j+=1) { if (!RGraph.isNullish(this.data[i][j]) && this.data[i][j][1] != null) { this.max = Math.max(this.max, typeof this.data[i][j][1] == 'object' ? RGraph.arrayMax(this.data[i][j][1]) : Math.abs(this.data[i][j][1])); } } } this.min = properties.yaxisScaleMin ? properties.yaxisScaleMin : 0; this.scale2 = RGraph.getScale({object: this, options: { 'scale.max': this.max, 'scale.min': this.min, '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.min = this.scale2.min; } this.grapharea = this.canvas.height - this.marginTop - this.marginBottom; // // Install clipping // // MUST be the first thing that's done after the // beforedraw event // if (!RGraph.isNullish(this.properties.clip)) { RGraph.clipTo.start(this, this.properties.clip); } // Progressively Draw the chart RGraph.Background.draw(this); // // Draw any horizontal bars that have been specified // if (properties.backgroundHbars && properties.backgroundHbars.length) { RGraph.drawBars(this); } // // Draw any vertical bars that have been specified // if (properties.backgroundVbars && properties.backgroundVbars.length) { this.drawVBars(); } // // Draw an X scale // if (!properties.xaxisScaleMax) { var xmax = 0; var xmin = properties.xaxisScaleMin; for (var ds=0,len=this.data.length; ds<len; ds+=1) { for (var point=0,len2=this.data[ds].length; point<len2; point+=1) { xmax = Math.max(xmax, this.data[ds][point][0]); } } } else { xmax = properties.xaxisScaleMax; xmin = properties.xaxisScaleMin } if (properties.xaxisScale) { this.xscale2 = RGraph.getScale({object: this, options: { 'scale.max': xmax, 'scale.min': xmin, 'scale.decimals': properties.xaxisScaleDecimals, 'scale.point': properties.xaxisScalePoint, 'scale.thousand': properties.xaxisScaleThousand, 'scale.units.pre': properties.xaxisScaleUnitsPre, 'scale.units.post': properties.xaxisScaleUnitsPost, 'scale.labels.count': properties.xaxisLabelsCount, 'scale.strict': true }}); this.set('xaxisScaleMax', this.xscale2.max); } this.drawAxes(); this.drawLabels(); // Clip the canvas so that the trace2 effect is facilitated if (properties.animationTrace) { this.context.save(); this.context.beginPath(); this.context.rect(0, 0, this.canvas.width * properties.animationTraceClip, this.canvas.height); this.context.clip(); } for(i=0; i<this.data.length; ++i) { this.drawMarks(i); // Draw bubbles if (RGraph.isArray(properties.bubbleData)) { this.drawBubble(i); } // Set the shadow this.context.shadowColor = properties.lineShadowColor; this.context.shadowOffsetX = properties.lineShadowOffsetx; this.context.shadowOffsetY = properties.lineShadowOffsety; this.context.shadowBlur = properties.lineShadowBlur; this.drawLine(i); // Turn the shadow off RGraph.noShadow(this); } if (properties.line) { for (var i=0,len=this.data.length;i<len; i+=1) { this.drawMarks(i); // Call this again so the tickmarks appear over the line } } // // Draw a trendline if requested // if (properties.trendline) { for (var i=0; i<this.data.length; ++i) { if (properties.trendline === true || (typeof properties.trendline === 'object' && properties.trendline[i] === true) ) { this.drawTrendline(i); } } } if (properties.animationTrace) { this.context.restore(); } // // Setup the context menu if required // if (properties.contextmenu) { RGraph.showContext(this); } // // Draw the key if necessary // if (properties.key && properties.key.length) { RGraph.drawKey( this, properties.key, this.isMarimekko ? properties.marimekkoColors : properties.lineColors ); } // // Draw " above" labels if enabled // if (properties.labelsAbove) { this.drawAboveLabels(); } // // Draw the "in graph" labels, using the member function, NOT the shared function in RGraph.common.core.js // this.drawInGraphLabels(this); // // Add custom text thats specified // RGraph.addCustomText(this); // Draw any custom lines that have been defined RGraph.drawHorizontalLines(this); // Draw any custom lines that have been defined this.installLasso(); // // Draw a Marimekko chart // if (this.isMarimekko) { this.drawMarimekko(); } // // This installs the event listeners // RGraph.installEventListeners(this); // // End clipping // if (!RGraph.isNullish(this.properties.clip)) { 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; } // // Draws the axes of the scatter graph // this.drawAxes = function () { this.context.lineCap = 'square'; // Draw the X axis RGraph.drawXAxis(this); // Draw the Y axis RGraph.drawYAxis(this); // Reset the linewidth back to one // this.context.lineWidth = 1; }; // // Draws the labels on the scatter graph // this.drawLabels = function () { // Nothing to do here because the labels are drawn in the RGraph.drawXAxis() and // RGrapth.drawYAxis functions }; // // Draws the actual scatter graph marks // // @param i integer The dataset index // this.drawMarks = function (i) { // // Reset the coords array // this.coords[i] = []; // // Plot the values // var xmax = properties.xaxisScaleMax; var default_color = properties.colorsDefault; for (var j=0,len=this.data[i].length; j<len; j+=1) { // // This is here because tooltips are optional // var data_points = this.data[i]; // Allow for null points if (RGraph.isNullish(data_points[j])) { continue; } var xCoord = data_points[j][0]; var yCoord = data_points[j][1]; var color = data_points[j][2] ? data_points[j][2] : default_color; var tooltip = (data_points[j] && data_points[j][3]) ? data_points[j][3] : null; this.drawMark( i, xCoord, yCoord, xmax, this.scale2.max, color, tooltip, this.coords[i], data_points, j ); } }; // // Draws a single scatter mark // this.drawMark = function (data_set_index, x, y, xMax, yMax, color, tooltip, coords, data, data_index) { var tickmarks = properties.tickmarksStyle, tickSize = properties.tickmarksSize, xMin = properties.xaxisScaleMin, x = ((x - xMin) / (xMax - xMin)) * (this.canvas.width - this.marginLeft - this.marginRight), originalX = x, originalY = y; // // This allows the tickmarks property to be an array // if (tickmarks && typeof tickmarks == 'object') { tickmarks = tickmarks[data_set_index]; } // // This allows the ticksize property to be an array // if (typeof tickSize == 'object') { var tickSize = tickSize[data_set_index]; var halfTickSize = tickSize / 2; } else { var halfTickSize = tickSize / 2; } // // This bit is for boxplots only // if ( y && typeof y === 'object' && typeof y[0] === 'number' && typeof y[1] === 'number' && typeof y[2] === 'number' && typeof y[3] === 'number' && typeof y[4] === 'number' ) { this.set('boxplot', true); var y0 = this.getYCoord(y[0], properties.outofbounds), y1 = this.getYCoord(y[1], properties.outofbounds), y2 = this.getYCoord(y[2], properties.outofbounds), y3 = this.getYCoord(y[3], properties.outofbounds), y4 = this.getYCoord(y[4], properties.outofbounds), col1 = y[5], col2 = y[6], boxWidth = typeof y[7] == 'number' ? y[7] : properties.boxplotWidth; } else { // // The new way of getting the Y coord. This function (should) handle everything // var yCoord = this.getYCoord(y, properties.outofbounds); } // // Account for the X axis being at the centre // // This is so that points are on the graph, and not the gutter - which helps x += this.marginLeft; this.context.beginPath(); // Color this.context.strokeStyle = color; // // Boxplots // if (properties.boxplot) { // boxWidth is a scale value, so convert it to a pixel vlue boxWidth = (boxWidth / properties.xaxisScaleMax) * (this.canvas.width - this.marginLeft - this.marginRight); var halfBoxWidth = boxWidth / 2; if (properties.lineVisible) { this.context.beginPath(); // Set the outline color of the box if (typeof y[8] === 'string') { this.context.strokeStyle = y[8]; } this.context.strokeRect(x - halfBoxWidth, y1, boxWidth, y3 - y1); // Draw the upper coloured box if a value is specified if (col1) { this.context.fillStyle = col1; this.context.fillRect(x - halfBoxWidth, y1, boxWidth, y2 - y1); } // Draw the lower coloured box if a value is specified if (col2) { this.context.fillStyle = col2; this.context.fillRect(x - halfBoxWidth, y2, boxWidth, y3 - y2); } this.context.stroke(); // Now draw the whiskers this.context.beginPath(); if (properties.boxplotCapped) { this.context.moveTo(x - halfBoxWidth, Math.round(y0)); this.context.lineTo(x + halfBoxWidth, Math.round(y0)); } this.context.moveTo(Math.round(x), y0); this.context.lineTo(Math.round(x ), y1); if (properties.boxplotCapped) { this.context.moveTo(x - halfBoxWidth, Math.round(y4)); this.context.lineTo(x + halfBoxWidth, Math.round(y4)); } this.context.moveTo(Math.round(x), y4); this.context.lineTo(Math.round(x), y3); this.context.stroke(); } } // // Draw the tickmark, but not for boxplots // if (properties.lineVisible && typeof y == 'number' && !y0 && !y1 && !y2 && !y3 && !y4) { if (tickmarks == 'circle') { this.context.arc(x, yCoord, halfTickSize, 0, 6.28, 0); this.context.fillStyle = color; this.context.fill(); } else if (tickmarks == 'plus') { this.context.moveTo(x, yCoord - halfTickSize); this.context.lineTo(x, yCoord + halfTickSize); this.context.moveTo(x - halfTickSize, yCoord); this.context.lineTo(x + halfTickSize, yCoord); this.context.stroke(); } else if (tickmarks == 'square') { this.context.strokeStyle = color; this.context.fillStyle = color; this.context.fillRect( x - halfTickSize, yCoord - halfTickSize, tickSize, tickSize ); } else if (tickmarks == 'cross') { this.context.moveTo(x - halfTickSize, yCoord - halfTickSize); this.context.lineTo(x + halfTickSize, yCoord + halfTickSize); this.context.moveTo(x + halfTickSize, yCoord - halfTickSize); this.context.lineTo(x - halfTickSize, yCoord + halfTickSize); this.context.stroke(); // // Diamond shape tickmarks // } else if (tickmarks == 'diamond') { this.context.fillStyle = this.context.strokeStyle; this.context.moveTo(x, yCoord - halfTickSize); this.context.lineTo(x + halfTickSize, yCoord); this.context.lineTo(x, yCoord + halfTickSize); this.context.lineTo(x - halfTickSize, yCoord); this.context.lineTo(x, yCoord - halfTickSize); this.context.fill(); this.context.stroke(); // // Custom tickmark style // } else if (typeof tickmarks == 'function') { var graphWidth = this.canvas.width - this.marginLeft - this.marginRight, graphheight = this.canvas.height - this.marginTop - this.marginBottom, xVal = ((x - this.marginLeft) / graphWidth) * xMax, yVal = ((graphheight - (yCoord - this.marginTop)) / graphheight) * yMax; tickmarks(this, data, x, yCoord, xVal, yVal, xMax, yMax, color, data_set_index, data_index) // // 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/' ) ) { var img = new Image(); if (tickmarks.substr(0, 6) === 'image:') { img.src = tickmarks.substr(6); } else { img.src = tickmarks; } var obj = this; img.onload = function () { if (properties.tickmarksStyleImageHalign === 'center') x -= (this.width / 2); if (properties.tickmarksStyleImageHalign === 'right') x -= this.width; if (properties.tickmarksStyleImageValign === 'center') yCoord -= (this.height / 2); if (properties.tickmarksStyleImageValign === 'bottom') yCoord -= this.height; x += properties.tickmarksStyleImageOffsetx; yCoord += properties.tickmarksStyleImageOffsety; obj.context.drawImage(this, x, yCoord); } // // No tickmarks // } else if (tickmarks === null) { // // Unknown tickmark type // } else { alert('[SCATTER] (' + this.id + ') Unknown tickmark style: ' + tickmarks ); } } // // Add the tickmark to the coords array // if ( properties.boxplot && typeof y0 === 'number' && typeof y1 === 'number' && typeof y2 === 'number' && typeof y3 === 'number' && typeof y4 === 'number') { x = [x - halfBoxWidth, x + halfBoxWidth]; yCoord = [y0, y1, y2, y3, y4]; } coords.push([x, yCoord, tooltip]); }; // // Draws an optional line connecting the tick marks. // // @param i The index of the dataset to use // this.drawLine = function (i) { if (typeof properties.lineVisible == 'boolean' && properties.lineVisible == false) { return; } if (properties.line && this.coords[i].length >= 2) { if (properties.lineDash && typeof this.context.setLineDash === 'function') { this.context.setLineDash(properties.lineDash); } this.context.lineCap = 'round'; this.context.lineJoin = 'round'; this.context.lineWidth = this.getLineWidth(i);// i is the index of the set of coordinates this.context.strokeStyle = properties.lineColors[i]; this.context.beginPath(); var prevY = null; var currY = null; for (var j=0,len=this.coords[i].length; j<len; j+=1) { var xPos = this.coords[i][j][0]; var yPos = this.coords[i][j][1]; if (j > 0) prevY = this.coords[i][j - 1][1]; currY = yPos; if (j == 0 || RGraph.isNullish(prevY) || RGraph.isNullish(currY)) { this.context.moveTo(xPos, yPos); } else { // Stepped? var stepped = properties.lineStepped; if ( (typeof stepped == 'boolean' && stepped) || (typeof stepped == 'object' && stepped[i]) ) { this.context.lineTo(this.coords[i][j][0], this.coords[i][j - 1][1]); } this.context.lineTo(xPos, yPos); } } this.context.stroke(); // // Set the linedash back to the default // if (properties.lineDash && typeof this.context.setLineDash === 'function') { //this.context.setLineDash([1,0]); this.context.setLineDash([]); } } // // Set the linewidth back to 1 // this.context.lineWidth = 1; }; // // Returns the linewidth // // @param number i The index of the "line" (/set of coordinates) // this.getLineWidth = function (i) { var linewidth = properties.lineLinewidth; if (typeof linewidth == 'number') { return linewidth; } else if (typeof linewidth == 'object') { if (linewidth[i]) { return linewidth[i]; } else { return linewidth[0]; } alert('[SCATTER] Error! The linewidth property should be a single number or an array of one or more numbers'); } }; // // Draws vertical bars. Line chart doesn't use a horizontal scale, hence this function // is not common // this.drawVBars = function () { var vbars = properties.backgroundVbars; var graphWidth = this.canvas.width - this.marginLeft - this.marginRight; if (vbars) { var xmax = properties.xaxisScaleMax; var xmin = properties.xaxisScaleMin; for (var i=0,len=vbars.length; i<len; i+=1) { var key = i; var value = vbars[key]; // // Accomodate date/time values // if (typeof value[0] == 'string') value[0] = RGraph.parseDate(value[0]); if (typeof value[1] == 'string') value[1] = RGraph.parseDate(value[1]) - value[0]; var x = (( (value[0] - xmin) / (xmax - xmin) ) * graphWidth) + this.marginLeft; var width = (value[1] / (xmax - xmin) ) * graphWidth; this.context.fillStyle = value[2]; this.context.fillRect(x, this.marginTop, width, (this.canvas.height - this.marginTop - this.marginBottom)); } } }; // // Draws in-graph labels. // // @param object obj The graph object // this.drawInGraphLabels = function (obj) { var labels = obj.get('labelsIngraph'); var labels_processed = []; if (!labels) { return; } // Defaults var fgcolor = 'black'; var bgcolor = 'white'; var direction = 1; var textConf = RGraph.getTextConf({ object: this, prefix: 'labelsIngraph' }); // // Preprocess the labels array. Numbers are expanded // for (var i=0,len=labels.length; i<len; i+=1) { if (typeof labels[i] == 'number') { for (var j=0; j<labels[i]; ++j) { labels_processed.push(null); } } else if (typeof labels[i] == 'string' || typeof labels[i] == 'object') { labels_processed.push(labels[i]); } else { labels_processed.push(''); } } // // Turn off any shadow // RGraph.noShadow(obj); if (labels_processed && labels_processed.length > 0) { var i=0; for (var set=0; set<obj.coords.length; ++set) { for (var point = 0; point<obj.coords[set].length; ++point) { if (labels_processed[i]) { var x = obj.coords[set][point][0] + properties.labelsIngraphOffsetx; var y = obj.coords[set][point][1] + properties.labelsIngraphOffsety; var length = typeof labels_processed[i][4] == 'number' ? labels_processed[i][4] : 25; var text_x = x; var text_y = y - 5 - length; this.context.moveTo(x, y - 5); this.context.lineTo(x, y - 5 - length); this.context.stroke(); this.context.beginPath(); // This draws the arrow this.context.moveTo(x, y - 5); this.context.lineTo(x - 3, y - 10); this.context.lineTo(x + 3, y - 10); this.context.closePath(); this.context.beginPath(); // Fore ground color this.context.fillStyle = (typeof labels_processed[i] == 'object' && typeof labels_processed[i][1] == 'string') ? labels_processed[i][1] : 'black'; RGraph.text({ object: this, font: textConf.font, size: textConf.size, color: textConf.color, bold: textConf.bold, italic: textConf.italic, x: text_x, y: text_y, text: (typeof labels_processed[i] == 'object' && typeof labels_processed[i][0] == 'string') ? labels_processed[i][0] : labels_processed[i], valign: 'bottom', halign: 'center', bounding: true, boundingFill: (typeof labels_processed[i] == 'object' && typeof labels_processed[i][2] == 'string') ? labels_processed[i][2] : 'white', tag: 'labels.ingraph' }); this.context.fill(); } i++; } } } }; // // This function makes it much easier to get the (if any) point that is currently being hovered over. // // @param object e The event object // this.getShape = function (e) { var mouseXY = RGraph.getMouseXY(e); var mouseX = mouseXY[0]; var mouseY = mouseXY[1]; var overHotspot = false; var offset = properties.tooltipsHotspot; // This is how far the hotspot extends if (this.isMarimekko) { for (var i=0,seq=0; i<this.coordsMarimekko.length; ++i) { for (var j=0; j<this.coordsMarimekko[i].length; ++j) { let coords = this.coordsMarimekko[i][j]; if ( mouseX > coords[0] && mouseX < (coords[0] + coords[2]) && mouseY > coords[1] && mouseY < (coords[1] + coords[3]) && (this.properties.clip ? RGraph.clipTo.test(this, mouseX, mouseY) : true) ) { // Determine the tooltip var tooltip = null; if (RGraph.isString(this.get('marimekkoTooltips'))) { tooltip = this.get('marimekkoTooltips'); } else if ( RGraph.isArray(this.get('marimekkoTooltips')) && RGraph.isString(this.get('marimekkoTooltips')[seq])) { tooltip = this.get('marimekkoTooltips')[seq]; } if (RGraph.parseTooltipText) { tooltip = RGraph.parseTooltipText(tooltip, seq); } // Return the shape array return { object: this, x: coords[0], y: coords[1], width: coords[2], height: coords[3], tooltip: tooltip, dataset: i, index: j, sequentialIndex: seq }; } ++seq; } } } else { for (var set=0,len=this.coords.length; set<len; ++set) { for (var i=0,len2=this.coords[set].length; i<len2; ++i) { var x = this.coords[set][i][0]; var y = this.coords[set][i][1]; var tooltip = this.data[set][i][3]; var seq = this.dataIndexMapGroupedToSequential[set][i]; var bubbleMin = this.properties.bubbleMin; var bubbleMax = this.properties.bubbleMax; var bubbleData = this.properties.bubbleData; var maxWidth = this.properties.bubbleWidth; var isBubble = RGraph.isNumber(bubbleMin) && RGraph.isNumber(bubbleMax) && RGraph.isArray(bubbleData) && RGraph.isNumber(bubbleData[seq]); if (isBubble) { // Get the width of the bubble from the coordinates var bubbleWidth = this.coordsBubble[set][i][2]; // Get the mouse distance from the center // of the point var hypLength = RGraph.getHypLength(mouseX, mouseY, x, y); } // Add highlight so that we can observe the hotspot //this.path('lw 1 b a % % % 0 6.29 false s gray', x, y, bubbleWidth) if (typeof y == 'number') { if ( ((isBubble && hypLength <= bubbleWidth) || (mouseX <= (x + offset) && mouseX >= (x - offset) && mouseY <= (y + offset) && mouseY >= (y - offset))) && (this.properties.clip ? RGraph.clipTo.test(this, mouseX, mouseY) : true)) { if (RGraph.parseTooltipText) { var tooltip = RGraph.parseTooltipText(this.data[set][i][3], 0); } var sequentialIndex = i; for (var ds=(set-1); ds >=0; --ds) { sequentialIndex += this.data[ds].length; } // Should the point be ignored? if (RGraph.tooltipsHotspotIgnore(this, sequentialIndex)) { return; } return { object: this, x: x, y: y, tooltip: typeof tooltip === 'string' ? tooltip : null, dataset: set, index: i, sequentialIndex: sequentialIndex }; } } else if (RGraph.isNullish(y)) { // Nothing to see here // Boxplots } else { var mark = this.data[set][i]; // // Determine the width // var width = properties.boxplotWidth; if (typeof mark[1][7] === 'number') { width = mark[1][7]; } if ( typeof x === 'object' && mouseX > x[0] && mouseX < x[1] && mouseY > y[1] && mouseY < y[3] ) { var tooltip = RGraph.parseTooltipText(this.data[set][i][3], 0); // Determine the sequential index var sequentialIndex = i; for (var ds=(set-1); ds >=0; --ds) { sequentialIndex += this.data[ds].length; } return { object: this, x: x[0], y: y[3], width: Math.abs(x[1] - x[0]), height: Math.abs(y[1] - y[3]), dataset: set, index: i, sequentialIndex: sequentialIndex, tooltip: tooltip }; } } } } } }; // // Draws the above line labels // this.drawAboveLabels = function () { var size = properties.labelsAboveSize; var font = properties.textFont; var units_pre = properties.yaxisScaleUnitsPre; var units_post = properties.yaxisScaleUnitsPost; var textConf = RGraph.getTextConf({ object: this, prefix: 'labelsAbove' }); for (var set=0,len=this.coords.length; set<len; ++set) { for (var point=0,len2=this.coords[set].length; point<len2; ++point) { var x_val = this.data[set][point][0]; var y_val = this.data[set][point][1]; if (!RGraph.isNullish(y_val)) { // Use the top most value from a box plot if (RGraph.isArray(y_val)) { var max = 0; for (var i=0; i<y_val; ++i) { max = Math.max(max, y_val[i]); } y_val = max; } var x_pos = this.coords[set][point][0]; var y_pos = this.coords[set][point][1]; var xvalueFormatter = properties.labelsAboveFormatterX; var yvalueFormatter = properties.labelsAboveFormatterY; RGraph.text({ object: this, font: textConf.font, size: textConf.size, color: textConf.color, bold: textConf.bold, italic: textConf.italic, x: x_pos + properties.labelsAboveOffsetx, y: y_pos - 5 - size + properties.labelsAboveOffsety, text: (typeof xvalueFormatter === 'function' ? xvalueFormatter(this, x_val) : x_val.toFixed(properties.labelsAboveDecimals)) + ', ' + (typeof yvalueFormatter === 'function' ? yvalueFormatter(this, y_val) : y_val.toFixed(properties.labelsAboveDecimals)), valign: 'bottom', halign: 'center', bounding: true, boundingFill: 'rgba(255, 255, 255, 0.7)', boundingStroke: 'rgba(0,0,0,0.1)', tag: 'labels.above' }); } } } }; // // 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 within the Bars. // // @param object e The event object // this.getYValue = 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; if ( mouseY < this.marginTop || mouseY > (this.canvas.height - this.marginBottom) //|| mouseX < this.marginLeft //|| mouseX > (this.canvas.width - this.marginRight) ) { return null; } if (properties.xaxisPosition == 'center') { var value = (((this.grapharea / 2) - (mouseY - this.marginTop)) / this.grapharea) * (this.max - this.min) value *= 2; // Account for each side of the X axis if (value >= 0) { value += this.min if (properties.yaxisScaleInvert) { value -= this.min; value = this.max - value; } } else { value -= this.min; if (properties.yaxisScaleInvert) { value += this.min; value = this.max + value; value *= -1; } } } else { var value = ((this.grapharea - (mouseY - this.marginTop)) / this.grapharea) * (this.max - this.min) value += this.min; if (properties.yaxisScaleInvert) { value -= this.min; value = this.max - value; } } return value; }; // // When you click on the chart, this method can return the X value at that point. // // @param mixed arg This can either be an event object or the X coordinate // @param number If specifying the X coord as the first arg then this should be the Y coord // this.getXValue = function (arg) { if (arg.length == 2) { var mouseX = arg[0]; var mouseY = arg[1]; } else { var mouseXY = RGraph.getMouseXY(arg); var mouseX = mouseXY[0]; var mouseY = mouseXY[1]; } var obj = this; if (//|| mouseY < this.marginTop //|| mouseY > (this.canvas.height - this.marginBottom) mouseX < this.marginLeft || mouseX > (this.canvas.width - this.marginRight) ) { return null; } var width = (this.canvas.width - this.marginLeft - this.marginRight); var value = ((mouseX - this.marginLeft) / width) * (properties.xaxisScaleMax - properties.xaxisScaleMin) value += properties.xaxisScaleMin; 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 (typeof properties.highlightStyle === 'function') { (properties.highlightStyle)(shape); // Inverted highlight style } else if (properties.highlightStyle === 'invert') { var tickmarksSize = properties.tickmarksSize; // Clip to the graph area this.path( 'sa b r % % % % cl', properties.marginLeft - tickmarksSize, properties.marginTop - tickmarksSize, this.canvas.width - properties.marginLeft - properties.marginRight + tickmarksSize + tickmarksSize, this.canvas.height - properties.marginTop - properties.marginBottom + tickmarksSize + tickmarksSize ); 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, 0, this.canvas.width, 0, this.canvas.width, this.canvas.height, 0, this.canvas.height, 0, 0, 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 ); } else { // Boxplot highlight if (shape.height) { RGraph.Highlight.rect(this, shape); // Bubble chart highlight } else if ( RGraph.isNumber(this.properties.bubbleMin) && RGraph.isNumber(this.properties.bubbleMax) && RGraph.isNumber(this.properties.bubbleWidth) && RGraph.isArray(this.properties.bubbleData) && RGraph.isNumber(this.properties.bubbleData[shape.sequentialIndex]) ) { var value = this.properties.bubbleData[shape.sequentialIndex]; var min = this.properties.bubbleMin; var max = this.properties.bubbleMax; var width = this.properties.bubbleWidth; this.properties.highlightPointRadius = (value - min) / (max - min) * width / 2; RGraph.Highlight.point(this, shape); // Point highlight } else { this.properties.highlightPointRadius = this.properties.tickmarksSize; 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); if ( mouseXY[0] > (this.marginLeft - 3) && mouseXY[0] < (this.canvas.width - this.marginRight + 3) && mouseXY[1] > (this.marginTop - 3) && mouseXY[1] < ((this.canvas.height - this.marginBottom) + 3) ) { return this; // // Do this if a bubble chart is being shown to // accommodate bubbles that are outside of the // graphArea // } else if ( RGraph.isArray(this.properties.bubbleData) && RGraph.isNumber(this.properties.bubbleMax) && RGraph.isNumber(this.properties.bubbleMin) && RGraph.isNumber(this.properties.bubbleWidth) ) { // Draw a rectangle which matches the // graphArea - except that it's bigger by half-a-bubbles // width to accommodate them poking out at the adges of // the canvas var halfBubble = this.properties.bubbleWidth / 2; if ( mouseXY[0] > (this.marginLeft - halfBubble) && mouseXY[0] < (this.canvas.width - this.marginRight + halfBubble) && mouseXY[1] > (this.marginTop - halfBubble) && mouseXY[1] < ((this.canvas.height - this.marginBottom) + halfBubble) ) { return this; } } }; // // This function can be used when the canvas is clicked on (or similar - depending on the event) // to retrieve the relevant X coordinate for a particular value. // // @param int value The value to get the X coordinate for // this.getXCoord = function (value) { if (typeof value != 'number' && typeof value != 'string') { return null; } // Allow for date strings to be passed if (typeof value === 'string') { value = RGraph.parseDate(value); } var xmin = properties.xaxisScaleMin; var xmax = properties.xaxisScaleMax; var x; if (value < xmin) return null; if (value > xmax) return null; if (properties.yaxisPosition == 'right') { x = ((value - xmin) / (xmax - xmin)) * (this.canvas.width - this.marginLeft - this.marginRight); x = (this.canvas.width - this.marginRight - x); } else { x = ((value - xmin) / (xmax - xmin)) * (this.canvas.width - this.marginLeft - this.marginRight); x = x + this.marginLeft; } return x; }; // // Returns the applicable Y COORDINATE when given a Y value // // @param int value The value to use // @return int The appropriate Y coordinate // this.getYCoord = function (value) { var outofbounds = arguments[1]; if (typeof value != 'number') { return null; } var invert = properties.yaxisScaleInvert; var xaxispos = properties.xaxisPosition; var graphHeight = this.canvas.height - this.marginTop - this.marginBottom; var halfGraphHeight = graphHeight / 2; var ymax = this.max; var ymin = properties.yaxisScaleMin; var coord = 0; if ( (value > ymax && !outofbounds) || (properties.xaxisPosition === 'bottom' && value < ymin && !outofbounds) || (properties.xaxisPosition === 'center' && ((value > 0 && value < ymin) || (value < 0 && value > (-1 * ymin)))) ) { return null; } // // This calculates scale values if the X axis is in the center // if (xaxispos == 'center') { coord = ((Math.abs(value) - ymin) / (ymax - ymin)) * halfGraphHeight; if (invert) { coord = halfGraphHeight - coord; } if (value < 0) { coord += this.marginTop; coord += halfGraphHeight; } else { coord = halfGraphHeight - coord; coord += this.marginTop; } // // And this calculates scale values when the X axis is at the bottom // } else { coord = ((value - ymin) / (ymax - ymin)) * graphHeight; if (invert) { coord = graphHeight - coord; } // Invert the coordinate because the Y scale starts at the top coord = graphHeight - coord; // And add on the top gutter coord = this.marginTop + coord; } return coord; }; // // Draws a bubble chart // // @param dataset int The dataset index // this.drawBubble = function (dataset) { // // First things first, linearize the bubbleData array. // Doing this allows you to give the array as both // linear and multi-dimensional (for qwhen you have // multiple datasets) // properties.bubbleData = RGraph.arrayLinearize(properties.bubbleData); // Now that the bubbleData is linearised this is not // really necessary //var data = RGraph.isArray(properties.bubbleData) && RGraph.isArray(properties.bubbleData[dataset]) ? properties.bubbleData[dataset] : properties.bubbleData; var data = this.properties.bubbleData; var min = RGraph.isArray(this.properties.bubbleMin) ? this.properties.bubbleMin[dataset] : this.properties.bubbleMin; var max = RGraph.isArray(this.properties.bubbleMax) ? this.properties.bubbleMax[dataset] : this.properties.bubbleMax; var width = RGraph.isArray(this.properties.bubbleWidth) ? this.properties.bubbleWidth[dataset] : this.properties.bubbleWidth; // Initialise the coordinates array this.coordsBubble[dataset] = []; // Loop through all the points (ALL datasets now) for (var index=0; index<this.coords[dataset].length; ++index) { // Get the sequential index var seq = this.dataIndexMapGroupedToSequential[dataset][index]; // // Is there a bubble data-piece for this point? // If not the skip it. // if (!RGraph.isNumber(data[seq])) { continue; } data[seq] = Math.max(data[seq], min); data[seq] = Math.min(data[seq], max); var radius = (((data[seq] - min) / (max - min) ) * width) / 2, color = this.data[dataset][index][2] ? this.data[dataset][index][2] : this.properties.colorsDefault; // Set a shadow for the bubbles if requested if (this.properties.bubbleShadow) { RGraph.setShadow({ object: this, prefix:'bubbleShadow' }); } this.context.beginPath(); this.context.fillStyle = RGraph.radialGradient({ object: this, x1: this.coords[dataset][index][0] + (radius / 2.5), y1: this.coords[dataset][index][1] - (radius / 2.5), r1: 0, x2: this.coords[dataset][index][0] + (radius / 2.5), y2: this.coords[dataset][index][1] - (radius / 2.5), r2: radius, colors: [ this.properties.colorsBubbleGraduated ? 'white' : color, color ] }); // Draw the bubble this.context.arc( this.coords[dataset][index][0], this.coords[dataset][index][1], radius, 0, RGraph.TWOPI, false ); if (this.properties.colorsBubbleStroke) { this.context.lineWidth = this.properties.bubbleLinewidth; this.context.strokeStyle = this.properties.colorsBubbleStroke; this.context.stroke(); } this.context.fill(); // Clear the shadow if (this.properties.bubbleShadow) { RGraph.noShadow(this); } this.coordsBubble[dataset][index] = [ this.coords[dataset][index][0], this.coords[dataset][index][1], radius, this.context.fillStyle ]; } }; // // This allows for easy specification of gradients // this.parseColors = function () { // Save the original colors so that they can be restored when the canvas is reset if (this.original_colors.length === 0) { this.original_colors.data = RGraph.arrayClone(this.data, true); this.original_colors.backgroundVbars = RGraph.arrayClone(properties.backgroundVbars, true); this.original_colors.backgroundHbars = RGraph.arrayClone(properties.backgroundHbars, true); this.original_colors.lineColors = RGraph.arrayClone(properties.lineColors, true); this.original_colors.colorsDefault = RGraph.arrayClone(properties.colorsDefault, true); this.original_colors.crosshairsColor = RGraph.arrayClone(properties.crosshairsColor, true); this.original_colors.highlightStroke = RGraph.arrayClone(properties.highlightStroke, true); this.original_colors.highlightFill = RGraph.arrayClone(properties.highlightFill, true); this.original_colors.backgroundBarsColor1 = RGraph.arrayClone(properties.backgroundBarsColor1, true); this.original_colors.backgroundBarsColor2 = RGraph.arrayClone(properties.backgroundBarsColor2, true); this.original_colors.backgroundGridColor = RGraph.arrayClone(properties.backgroundGridColor, true); this.original_colors.backgroundColor = RGraph.arrayClone(properties.backgroundColor, true); this.original_colors.axesColor = RGraph.arrayClone(properties.axesColor, true); this.original_colors.marimekkoColors = RGraph.arrayClone(properties.marimekkoColors, true); this.original_colors.marimekkoColorsStroke= RGraph.arrayClone(properties.marimekkoColorsStroke, true); this.original_colors.marimekkoLabelsIngraphBackgroundStroke= RGraph.arrayClone(properties.marimekkoLabelsIngraphBackgroundStroke, true); this.original_colors.marimekkoLabelsIngraphBackgroundFill = RGraph.arrayClone(properties.marimekkoLabelsIngraphBackgroundFill, true); } // Colors var data = this.data; if (data) { for (var dataset=0; dataset<data.length; ++dataset) { for (var i=0; i<this.data[dataset].length; ++i) { // Boxplots if (this.data[dataset][i] && typeof this.data[dataset][i][1] == 'object' && this.data[dataset][i][1]) { if (typeof this.data[dataset][i][1][5] == 'string') this.data[dataset][i][1][5] = this.parseSingleColorForGradient(this.data[dataset][i][1][5]); if (typeof this.data[dataset][i][1][6] == 'string') this.data[dataset][i][1][6] = this.parseSingleColorForGradient(this.data[dataset][i][1][6]); } if (!RGraph.isNullish(this.data[dataset][i])) { this.data[dataset][i][2] = this.parseSingleColorForGradient(this.data[dataset][i][2]); } } } } // Parse HBars var hbars = properties.backgroundHbars; if (hbars) { for (i=0; i<hbars.length; ++i) { hbars[i][2] = this.parseSingleColorForGradient(hbars[i][2]); } } // Parse HBars var vbars = properties.backgroundVbars; if (vbars) { for (i=0; i<vbars.length; ++i) { vbars[i][2] = this.parseSingleColorForGradient(vbars[i][2]); } } // Parse line colors var colors = properties.lineColors; if (colors) { for (i=0; i<colors.length; ++i) { colors[i] = this.parseSingleColorForGradient(colors[i]); } } // Parse colors for marimekko charts var colors = properties.marimekkoColors; if (colors) { for (i=0; i<colors.length; ++i) { colors[i] = this.parseSingleColorForGradient(colors[i]); } } properties.colorsDefault = this.parseSingleColorForGradient(properties.colorsDefault); properties.crosshairsColor = this.parseSingleColorForGradient(properties.crosshairsColor); properties.highlightStroke = this.parseSingleColorForGradient(properties.highlightStroke); properties.highlightFill = this.parseSingleColorForGradient(properties.highlightFill); properties.backgroundBarsColor1 = this.parseSingleColorForGradient(properties.backgroundBarsColor1); properties.backgroundBarsColor2 = this.parseSingleColorForGradient(properties.backgroundBarsColor2); properties.backgroundGridColor = this.parseSingleColorForGradient(properties.backgroundGridColor); properties.backgroundColor = this.parseSingleColorForGradient(properties.backgroundColor); properties.axesColor = this.parseSingleColorForGradient(properties.axesColor); properties.marimekkoColorsStroke = this.parseSingleColorForGradient(properties.marimekkoColorsStroke); properties.marimekkoLabelsIngraphBackgroundStroke = this.parseSingleColorForGradient(properties.marimekkoLabelsIngraphBackgroundStroke); properties.marimekkoLabelsIngraphBackgroundFill = this.parseSingleColorForGradient(properties.marimekkoLabelsIngraphBackgroundFill); }; // // 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 for a gradient // this.parseSingleColorForGradient = function (color) { if (!color || typeof color != 'string') { return color; } if (color.match(/^gradient\((.*)\)$/i)) { // Allow for JSON gradients if (color.match(/^gradient\(({.*})\)$/i)) { return RGraph.parseJSONGradient({object: this, def: RegExp.$1}); } // 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 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; }; // // 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) { if (this.coords && this.coords[index] && this.coords[index].length) { var obj = this; this.coords[index].forEach(function (value, idx, arr) { obj.context.beginPath(); obj.context.fillStyle = properties.keyInteractiveHighlightChartFill; obj.context.arc(value[0], value[1], properties.tickmarksSize + 3, 0, RGraph.TWOPI, false); obj.context.fill(); }); } }; // // 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 () { }; // // 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 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 color = properties.trendlineColor, linewidth = properties.trendlineLinewidth, margin = properties.trendlineMargin; // 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 += this.data[args.dataset][i][0]; totalY += this.data[args.dataset][i][1]; } var averageX = totalX / this.data[args.dataset].length; var averageY = totalY / this.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] = this.data[args.dataset][i][0] - averageX; yCoordMinusAverageY[i] = this.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 coords = [ [properties.xaxisScaleMin, m * properties.xaxisScaleMin + b], [properties.xaxisScaleMax, m * properties.xaxisScaleMax + b] ]; // // Draw the line // // Set dotted, dash or a custom dash array if (properties.trendlineDashed) { this.context.setLineDash([4,4]); } if (properties.trendlineDotted) { this.context.setLineDash([1,4]); } if (!RGraph.isNullish(properties.trendlineDashArray) && typeof properties.trendlineDashArray === 'object') { this.context.setLineDash(properties.trendlineDashArray); } // Clip the canvas again so that the line doesn't look overly long // (use the minimum an maximum points for this) for (var i=0,xValues=[],yValues=[]; i<this.data[args.dataset].length; ++i) { if (typeof this.data[args.dataset][i][0] === 'number') { xValues.push(this.data[args.dataset][i][0]); } if (typeof this.data[args.dataset][i][1] === 'number') { yValues.push(this.data[args.dataset][i][1]); } } // These are the minimum and maximum X/Y values for this dataset var x1 = RGraph.arrayMin({array: xValues}); var y1 = RGraph.arrayMin({array: yValues}); var x2 = RGraph.arrayMax({array: xValues}); var y2 = RGraph.arrayMax({array: yValues}); // Convert the X/Y values into coordinates on the canvas // // IS THIS USED? // x1 = this.getXCoord(x1); y1 = this.getYCoord(y1, properties.outofbounds); x2 = this.getXCoord(x2); y2 = this.getYCoord(y2, properties.outofbounds); // Draw the line this.path( ' lw % sa b r % % % % cl b r % % % % cl b m % % l % % s % rs', linewidth, // These are the rect arguments properties.marginLeft + margin, properties.marginTop + margin, this.canvas.width - properties.marginLeft - properties.marginRight - margin - margin, this.canvas.height - properties.marginTop - properties.marginBottom - margin - margin, // These are the second rect arguments properties.trendlineClipping === false ? 0 : x1 - 25, properties.trendlineClipping === false ? 0 : y2 - 25, properties.trendlineClipping === false ? this.canvas.width : x2 - x1 + 25 + 25, properties.trendlineClipping === false ? this.canvas.height : y1 - y2 + 25 + 25, // moveTo x1 = this.getXCoord(coords[0][0]), y1 = this.getYCoord(coords[0][1], true), // lineTo x2 = this.getXCoord(coords[1][0]), y2 = this.getYCoord(coords[1][1], true), // stroke color color ); // Store the coordinates of the trendline on the object this.coordsTrendline[args.dataset] = [ [x1, y1], [x2, y2] ]; // Reset the line dash array this.context.setLineDash([5,0]); }; // // The Scatter chart Trace effect // // 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, callback = arguments[2], opt = arguments[0] || {}, frames = opt.frames || 30, frame = 0, callback = arguments[1] || function () {} this.set('animationTrace', true); this.set('animationTraceClip', 0); 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', frame / frames); RGraph.Effects.updateCanvas(iterator); } else { callback(obj); } } iterator(); return this; }; // // The explode effect. // // @param object Options for the effect. // @param int A function that is called when the ffect is complete // this.explode = function () { // Cancel any stop request if one is pending this.cancelStopAnimation(); var obj = this, callback = arguments[2], opt = arguments[0] || {}, frames = opt.frames || 15, frame = 0, callback = arguments[1] || function () {}, originX = this.properties.xaxisScaleMax / 2, originY = 0, step = 1 / frames, original = RGraph.arrayClone(this.unmodified_data, true); // First draw the chart, set the yaxisScaleMax to the maximum value that's calculated // and then animate this.draw(); this.set('yaxisScaleMax', this.scale2.max); // Determine the X origin - it defaults to the center if (opt.originx === 'left') { originX = 0; } else if (opt.originx === 'right') { originX = this.properties.xaxisScaleMax; } else { originX = this.properties.xaxisScaleMax / 2; } // Determine the Y origin - it defaults to the bottom if (opt.originy === 'center') { originY = this.scale2.max / 2; } else if (opt.originy === 'top') { originY = this.scale2.max; } else { originY = 0; } function iterator () { if (obj.stopAnimationRequested) { // Reset the flag obj.stopAnimationRequested = false; return; } RGraph.clear(obj.canvas); for (var i=0; i<obj.data.length; ++i) { // Loop through each dataset for (var j=0; j<obj.data[i].length; ++j) { // Loop through each point obj.data[i][j][0] = originX + ( (original[i][j][0] - originX) * step * frame ); obj.data[i][j][1] = originY + ((original[i][j][1] - originY) * step * frame); } } RGraph.redrawCanvas(obj.canvas); if (frame++ < frames) { RGraph.Effects.updateCanvas(iterator); } else { 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 clip area this.set('animationTraceClip', 1); // Reset the data this.data = RGraph.arrayClone(this.unmodified_data, true); this.stopAnimationRequested = true; }; this.cancelStopAnimation = function () { this.stopAnimationRequested = false; }; // // This helps the Gantt reset colors when the reset function is called. // It handles going through the data and resetting the colors. // this.resetColorsToOriginalValues = function () { // // Copy the original colors over for single-event-per-line data // for (var i=0,len=this.original_colors.data.length; i<len; ++i) { for (var j=0,len2=this.original_colors.data[i].length; j<len2;++j) { // The color for the point this.data[i][j][2] = RGraph.arrayClone(this.original_colors.data[i][j][2], true); // Handle boxplots if (typeof this.data[i][j][1] === 'object') { this.data[i][j][1][5] = RGraph.arrayClone(this.original_colors.data[i][j][1][5], true); this.data[i][j][1][6] = RGraph.arrayClone(this.original_colors.data[i][j][1][6], true); } } } }; // If only one tooltip has been given populate each data-piece with it this.populateTooltips = function () { for (var i=0; i<this.data.length; ++i) { // for each dataset... for (var j=0; j<this.data[i].length; ++j) { // For each point in the dataset this.data[i][j][3] = properties.tooltips; } } }; // // A worker function that handles Bar chart specific tooltip substitutions // this.tooltipSubstitutions = function (opt) { // Create the data for marimekko charts if (this.isMarimekko) { var marimekko_data = []; for (var i=0; i<this.properties.marimekkoData.length; ++i) { marimekko_data[i] = []; for (var j=0; j<this.properties.marimekkoData[i][1].length; ++j) { marimekko_data[i].push(this.properties.marimekkoData[i][1][j]); } } } // Marimekko charts if (this.isMarimekko) { var indexes = RGraph.sequentialIndexToGrouped( opt.index, marimekko_data ); var value = marimekko_data[indexes[0]][indexes[1]]; var values = [marimekko_data[indexes[0]][indexes[1]]]; // Regular Scatter charts } else { var indexes = RGraph.sequentialIndexToGrouped( opt.index, this.data ); var value = this.data[indexes[0]][indexes[1]][1]; var values = [this.data[indexes[0]][indexes[1]][1]]; } return { index: indexes[1], dataset: indexes[0], sequentialIndex: opt.index, value: value, 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) { // The tooltipsFormattedKeyColors property has been specified so use that if // there's a relevant color if ( !RGraph.isNullish(properties.tooltipsFormattedKeyColors) && typeof properties.tooltipsFormattedKeyColors === 'object' && typeof properties.tooltipsFormattedKeyColors[specific.dataset] === 'string' ) { var color = properties.tooltipsFormattedKeyColors[specific.dataset]; } // If a color is defined for this point then use it if (!this.isMarimekko && this.data[specific.dataset][specific.index][2]) { color = this.data[specific.dataset][specific.index][2]; } var label = properties.tooltipsFormattedKeyLabels[specific.index] ? properties.tooltipsFormattedKeyLabels[specific.index] : ''; return { label: label, color: color }; }; // // This allows for static tooltip positioning // this.positionTooltipStatic = function (args) { // Make the data into a bog standard 2D array if (this.isMarimekko) { var marimekko_data = []; for (var i=0; i<this.properties.marimekkoData.length; ++i) { marimekko_data[i] = []; for (var j=0; j<this.properties.marimekkoData[i][1].length; ++j) { marimekko_data[i].push(this.properties.marimekkoData[i][1][j]); } } } var obj = args.object, e = args.event, tooltip = args.tooltip, index = args.index, canvasXY = RGraph.getCanvasXY(obj.canvas), indexes = RGraph.sequentialIndexToGrouped(args.index, this.isMarimekko ? marimekko_data : this.data), coords = this.isMarimekko ? this.coordsMarimekko[indexes[0]][indexes[1]] : this.coords[indexes[0]][indexes[1]]; // Position the tooltip in the X direction args.tooltip.style.left = ( canvasXY[0] // The X coordinate of the canvas + (this.isMarimekko ? (coords[0] + (coords[2] / 2) ) : 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'; }; // // 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 { var totalX = 0; var totalY = 0; if (this.data[index]) { for (var i=0; i<this.data[index].length; ++i) { totalX += this.data[index][i][0]; totalY += this.data[index][i][1]; } } return [totalX, totalY]; } }; // // 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[0].length; }; // // The lasso fature allows you to draw around points. // The callback is passed an array of datapoints and // indexes (NOT values!) which you can use to get the // points from the scatter chart object. // this.installLasso = function () { // Set this so that the event listeners can // access the object var obj = this; if (this.properties.lasso) { var localData_key = 'rgraph-scatter-chart-' + RGraph.md5(location.href + obj.id) + '-lasso-data'; // // So the default object isn't repeated // obj.lassoGetDefaultObject = function () { return {state: { coords: [], mousedown: false }}; }; // // Save the lasso data to localstorage so // that it persists acoss refreshes // obj.lassoSaveData = function (data) { // // Save the state to localStorage by // default // if (obj.properties.lassoPersistLocal) { var str = JSON.stringify(data); window.localStorage[localData_key] = str; } // Call the event if it's defined if (RGraph.isFunction(obj.properties.lassoPersistSave)) { obj.properties.lassoPersistSave(data); } }; // // Load the lasso data from localstorage // obj.lassoLoadData = function () { // Load the data from localData if (obj.properties.lassoPersistLocal) { var str = window.localStorage[localData_key]; } var data = str ? JSON.parse(str) : obj.lassoGetDefaultObject().state; // Call the event if it's defined if (RGraph.isFunction(obj.properties.lassoPersistLoad)) { data = obj.properties.lassoPersistLoad(); } return data; }; // // Reset the localStorageData // obj.lassoResetData = function () { window.localStorage[localData_key] = obj.lassoGetDefaultObject(); }; // // Initialise the lasso state // RGraph.runOnce('scatter-chart-lasso-state-object-initialisation', function () { // // Load the persistence data from the // localData array. Create an // obj.lassoLoadData() function. if (obj.properties.lassoPersist) { obj.lasso = {state: obj.lassoLoadData()}; } else { // This is repeated in the doubleclick/clear function below obj.lasso = obj.lassoGetDefaultObject(); } }); // // A function that draws the highlight // this.drawLassoRects = function () { if (!obj.lasso || !obj.lasso.state) { return; } // // Loop through the coords // for (let i=0; i<obj.lasso.state.coords.length; ++i) { if (obj.lasso.state.coords[i]) { // // Start the path // obj.path('b ss % fs % lw % lc square', obj.properties.lassoStroke, obj.properties.lassoFill, obj.properties.lassoLinewidth ); for (let j=0; j<obj.lasso.state.coords[i].length; ++j) { obj.context.rect( obj.lasso.state.coords[i][0], obj.lasso.state.coords[i][1], obj.lasso.state.coords[i][2], obj.lasso.state.coords[i][3] ); } // // Finish the and fill/stroke it // obj.context.closePath(); obj.context.fill(); obj.context.stroke(); } } }; // // Highlightpoints that have been selected // by the lasso // obj.drawLassoHighlightPoints = function (x, y = null) { // An array of points datasets/indexes has been given if (RGraph.isArray(x)) { for (let i=0; i<x.length; ++i) { obj.path( 'b lw % a % % % 0 6.29 false s % f %', obj.properties.lassoHighlightLinewidth, obj.coords[x[i].dataset][x[i].index][0], obj.coords[x[i].dataset][x[i].index][1], obj.properties.tickmarksSize / 2 + 2, obj.properties.lassoHighlightStroke, obj.properties.lassoHighlightFill ); } // An x/y coordinates combo has been given } else { obj.path( 'b lw % a % % % 0 6.29 false s % f %', obj.properties.lassoHighlightLinewidth, x, y, obj.properties.tickmarksSize / 2 + 2, obj.properties.lassoHighlightStroke, obj.properties.lassoHighlightFill ); } }; // // Clear the lasso state // this.clearLassoState = function () { if (confirm('Are you sure that you want to clear ALL of the highlight rectangles?')) { // This is repeated above obj.lasso = obj.lassoGetDefaultObject(); // // Save the reset state to localData // if persistence is enabled // if (obj.properties.lassoPersist) { obj.lassoSaveData(obj.lasso.state); } RGraph.redrawCanvas(obj.canvas); // Call the user defined function if necessary if (RGraph.isFunction(obj.properties.lassoClearCallback)) { obj.properties.lassoClearCallback(obj.lasso.state); } } }; // // This is the lasso mousedown event listener // this.lassoMousedownEventListener = function (e) { if (e.button === 2) { return; } var [mouseX, mouseY] = RGraph.getMouseXY(e); // Reset the state if (!obj.lasso || !obj.lasso.state) { obj.lasso = obj.lassoGetDefaultObject(); } obj.lasso.state.mousedown = true; obj.lasso.state.coordsLastIndex = obj.lasso.state.coords.length; // Create a new lasso coords array obj.lasso.state.originX = mouseX; obj.lasso.state.originY = mouseY; }; // // This is the lasso mousemove event listener // obj.lassoMousemoveEventListener = function (e) { var [mouseX, mouseY] = RGraph.getMouseXY(e); if ( obj.lasso && obj.lasso.state && obj.lasso.state.mousedown ) { RGraph.redrawCanvas(obj.canvas); // Store the coordinates of the // rectngle such that the X/Y // coordinates are always the top // left corner obj.lasso.state.coords[obj.lasso.state.coordsLastIndex] = [ mouseX > obj.lasso.state.originX ? obj.lasso.state.originX : mouseX, mouseY > obj.lasso.state.originY ? obj.lasso.state.originY : mouseY, Math.abs(mouseX - obj.lasso.state.originX), Math.abs(mouseY - obj.lasso.state.originY) ]; // Set the strokestyle and the fillstyle obj.path('b ss % fs % lw %', obj.properties.lassoStroke, obj.properties.lassoFill, obj.properties.lassoLinewidth ); // Highlight the already-selected points obj.drawLassoRects(); obj.drawLassoHighlightPoints(obj.lasso.state.points); } }; // // This is the lasso mouseup event listener - where // the magic happens! // this.lassoWindowMouseupEventListener = function (e) { if (obj.lasso && obj.lasso.state && obj.lasso.state.mousedown) { obj.lasso.state.mousedown = false; // Start a new path for the // rectangles that will be used to // test the points obj.path('b'); for (let i=0; i<obj.lasso.state.coords.length; ++i) { if (RGraph.isArray(obj.lasso.state.coords[i])) { obj.path( 'r % % % % f % s %', obj.lasso.state.coords[i][0], obj.lasso.state.coords[i][1], obj.lasso.state.coords[i][2], obj.lasso.state.coords[i][3], 'transparent', 'transparent' ); } } // Reset the points array to stop it growing // uncontrollably obj.lasso.state.points = []; // Determine all of the relevant points for (let i=0; i<obj.coords.length; ++i) { for (let j=0; j<obj.coords[i].length; ++j) { var [valueX, valueY] = obj.data[i][j]; var coordX = obj.getXCoord(valueX); var coordY = obj.getYCoord(valueY); if (obj.context.isPointInPath(coordX, coordY)) { obj.lasso.state.points = obj.lasso.state.points || []; obj.lasso.state.points.push({ dataset: i, index: j }); } } } // Now highlight the selected points if (obj.lasso.state.points) { for (let i=0; i<obj.lasso.state.points.length; ++i) { var dataset = obj.lasso.state.points[i].dataset; var index = obj.lasso.state.points[i].index; obj.drawLassoHighlightPoints( obj.coords[dataset][index][0], obj.coords[dataset][index][1] ); } } // // Save the highlight data to // localStorage // if (obj.properties.lassoPersist) { obj.lassoSaveData(obj.lasso.state); } // Call the callback function if (RGraph.isFunction(obj.properties.lassoCallback)) { obj.properties.lassoCallback(obj.lasso.state); } } }; // // Allow people to clear the lasso state using // a double-click // this.lassoDblclickEventListener = function (e) { // Go through all of the rects to see if one // has been clicked and if so - remove it. if (obj.lasso.state.coords && obj.lasso.state.coords.length) { var [mouseX, mouseY] = RGraph.getMouseXY(e); for (let i=(obj.lasso.state.coords.length - 1); i>=0; --i) { if ( RGraph.isArray(obj.lasso.state.coords) && obj.lasso.state.coords.length && obj.lasso.state.coords[i] && mouseX >= obj.lasso.state.coords[i][0] && mouseX <= (obj.lasso.state.coords[i][0] + obj.lasso.state.coords[i][2]) && mouseY >= obj.lasso.state.coords[i][1] && mouseY <= (obj.lasso.state.coords[i][1] + obj.lasso.state.coords[i][3]) ) { if (confirm('Are you sure that you want to remove this highlight rectangle?')) { obj.lasso.state.coords[i] = null; // Call the window mouseup // event listener to // recalulate the points which // should be highlighted obj.lasso.state.mousedown = true; obj.lassoWindowMouseupEventListener(e); //RGraph.redrawCanvas(obj.canvas); RGraph.redraw(); } return; } } } obj.clearLassoState(); }; // Install the above event listeners RGraph.runOnce('rgraph-install-scatter-chart-lasso-event-listeners', function () { obj.canvas.addEventListener('mousedown', obj.lassoMousedownEventListener, false); obj.canvas.addEventListener('mousemove', obj.lassoMousemoveEventListener, false); obj.canvas.addEventListener('dblclick', obj.lassoDblclickEventListener, false); window.addEventListener('mouseup', obj.lassoWindowMouseupEventListener, false); }); // If the mouse button isn't pressed then redraw the // rectangles and point highlights if ( this.lasso && this.lasso.state && !this.lasso.state.mousedown ) { this.drawLassoRects(); this.drawLassoHighlightPoints(this.lasso.state.points); } } }; // // Draws a Marimekko chart // this.drawMarimekko = function () { var data = RGraph.arrayClone(this.properties.marimekkoData, true); // Calculate the total of all the X values for (var i=0,totalX=0; i<data.length; ++i) { totalX += data[i][0]; } var graphWidth = this.canvas.width - this.properties.marginLeft - this.properties.marginRight; var graphHeight = this.canvas.height - this.properties.marginTop - this.properties.marginBottom; var x = this.properties.marginLeft; var coords = []; //////////////////////////////// // Draw the data on the chart // //////////////////////////////// for (var i=0,seq=0; i<data.length; ++i) { var width = (data[i][0] / totalX) * graphWidth; var y = this.canvas.height - this.properties.marginBottom; coords[i] = []; // Calulate the total Y value for (var j=0,totalY=0; j<data[i][1].length; ++j) { totalY += data[i][1][j]; } // Loop through the vertical values for (var j=0; j<data[i][1].length; ++j) { var value = data[i][1][j]; var height = (value / totalY) * graphHeight; var pc = (value / RGraph.arraySum(data[i][1])) * 100; this.path( 'b r % % % % lw % s % f %', x, y - height, width, height, this.properties.marimekkoLinewidth, this.properties.marimekkoLinewidth ? this.properties.marimekkoColorsStroke : 'transparent', this.properties.marimekkoColorsSequential ? this.properties.marimekkoColors[seq] : this.properties.marimekkoColors[j] ); coords[i].push([x, y - height, width, height]); // // Draw the ingraph label if requested // if (this.properties.marimekkoLabelsIngraph) { if (!marimekkoLabelsIngraphTextConf) { var marimekkoLabelsIngraphTextConf = RGraph.getTextConf({ object: this, prefix: 'marimekkoLabelsIngraph' }); } var text = (this.properties.marimekkoLabelsIngraphSpecific && RGraph.isString(this.properties.marimekkoLabelsIngraphSpecific[seq]) ) ? (this.properties.marimekkoLabelsIngraphSpecific[seq] || '') : RGraph.numberFormat({ object: this, number: Number(pc).toFixed(this.properties.marimekkoLabelsIngraphDecimals), value: Number(pc).toFixed(this.properties.marimekkoLabelsIngraphDecimals), unitspre: this.properties.marimekkoLabelsIngraphUnitsPre, unitspost: this.properties.marimekkoLabelsIngraphUnitsPost, point: this.properties.marimekkoLabelsIngraphPoint, thousand: this.properties.marimekkoLabelsIngraphThousand }); seq++; RGraph.text({ object: this, text: text, x: x + (width / 2) + this.properties.marimekkoLabelsIngraphOffsetx, y: y - (height / 2) + 5 + this.properties.marimekkoLabelsIngraphOffsety, color: marimekkoLabelsIngraphTextConf.color, italic: marimekkoLabelsIngraphTextConf.italic, bold: marimekkoLabelsIngraphTextConf.bold, font: marimekkoLabelsIngraphTextConf.font, size: marimekkoLabelsIngraphTextConf.size, halign: 'center', valign: 'center', tag: 'marimekkoLabelsIngraph', bounding: true, 'bounding.stroke': this.properties.marimekkoLabelsIngraphBackgroundStroke, 'bounding.fill': this.properties.marimekkoLabelsIngraphBackgroundFill }); } y -= height; } // // Draw the regular (horizonatal) Marimekko // label if it's been given // if (this.properties.marimekkoLabels) { var textConf = RGraph.getTextConf({ object: this, prefix: 'marimekkoLabels' }); // Get the label if (RGraph.isString(this.properties.marimekkoLabels)) { var label = this.properties.marimekkoLabels; } else if (RGraph.isArray(this.properties.marimekkoLabels)) { var label = this.properties.marimekkoLabels[i]; } else { var label = ''; } // Now do substitution on the label var label = RGraph.labelSubstitution({ index: i, text: label || '', object: this, value: RGraph.arraySum(data[i][1]), decimals: this.properties.marimekkoLabelsFormattedDecimals, point: this.properties.marimekkoLabelsFormattedPoint, thousand: this.properties.marimekkoLabelsFormattedThousand, unitsPre: this.properties.marimekkoLabelsFormattedUnitsPre, unitsPost: this.properties.marimekkoLabelsFormattedUnitsPost }); RGraph.text({ object: this, x: coords[i].at(-1)[0] + (coords[i].at(-1)[2] / 2) + this.properties.marimekkoLabelsOffsetx, y: coords[i].at(-1)[1] - 5 + this.properties.marimekkoLabelsOffsety, text: label, color: textConf.color, font: textConf.font, size: textConf.size, bold: textConf.bold, italic: textConf.italic, halign: 'center', valign: 'bottom' }); } x += width; } // // Store the coordinates // this.coordsMarimekko = coords; 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) { var shape = RGraph.Registry.get('adjusting.shape'); if (shape) { RGraph.Registry.set('adjusting.shape', shape); var x = this.getXValue(e); var y = this.getYValue(e); var [mouseX, mouseY] = RGraph.getMouseXY(e); // Y coordinate bounding if (mouseY <= shape.object.properties.marginTop) { y = shape.object.scale2.max; } else if (mouseY > (shape.object.canvas.height - shape.object.properties.marginBottom) ) { y = shape.object.scale2.min; } // X coordinate bounding if (mouseX <= shape.object.properties.marginLeft) { x = shape.object.properties.xaxisScaleMin; } else if (mouseX > (shape.object.canvas.width - shape.object.properties.marginRight) ) { x = shape.object.properties.xaxisScaleMax; } // Set the new X and Y values this.data[shape.dataset][shape.index][0] = x; this.data[shape.dataset][shape.index][1] = y; RGraph.redrawCanvas(e.target); RGraph.fireCustomEvent(this, 'onadjust'); } } }; // // 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 (shape && RGraph.isArray(properties.adjustableOnly) && properties.adjustableOnly[shape.sequentialIndex]) { return true; } return false; }; // // 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, true), y2 = this.getYCoord(to, true), 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 TESTIING 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.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, true), y2 = this.getYCoord(to, true), 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 // RGraph.register(this); // // This is the 'end' of the constructor so if the first argument // contains configuration data - handle that. // RGraph.parseObjectStyleConfig(this, conf.options); }; // // A shortcut function to generate some radom data for // Scatter chart points // // @param object conf The configuration that determines the // points on the chart // @param object An object of parameters. The obj can // contain the following keys: // o count: Number of points to generate (20) // o xmin: Minimum X value (0) // o xmax: Maximum X value (10) // o ymin: Minimum Y value (0) // o ymax: Maximum Y value (10) // o tooltip: Whether to generate a tooltip // index (false) // //@return An object of points suitable for the Scatter chart // RGraph.Scatter.random = function (conf) { // Defaults if (!RGraph.isObject(conf)) conf = {}; if (!RGraph.isNumber(conf.count)) conf.count = 20; if (!RGraph.isNumber(conf.xmin)) conf.xmin = 0; if (!RGraph.isNumber(conf.xmax)) conf.xmax = 10; if (!RGraph.isNumber(conf.ymin)) conf.ymin = 0; if (!RGraph.isNumber(conf.ymax)) conf.ymax = 10; if (!RGraph.isBoolean(conf.tooltip)) conf.tooltip = false; for (var i=0,data=[]; i<conf.count; ++i) { var arr = { x: RGraph.random(conf.xmin, conf.xmax), y: RGraph.random(conf.ymin, conf.ymax) }; if (conf.tooltip) { arr.tooltip = 'X value: {1} Y value: {2}'.format(arr.x, arr.y); } data.push(arr); } return data; }; // // This is a shortcut-style function that makes making // drilldown chart much easier for the user. // RGraph.Scatter.drilldown = function (opt) { var original = { xaxisScaleMin: opt.options.xaxisScaleMin, xaxisScaleMax: opt.options.xaxisScaleMax }; // // Set the initial max and min for the scale // opt.options.xaxisScaleMin = opt.options.xaxisScaleMin; opt.options.xaxisScaleMax = opt.options.xaxisScaleMax; // Create the Scatter chart (it's actually an XY line chart var obj = new RGraph.Scatter({ id: opt.id, data: RGraph.arrayClone(opt.data, true), options: opt.options }); if (opt.options.animation && typeof obj[opt.options.animation] === 'function') { obj[opt.options.animation](); } else { obj.draw(); } state = {}; // The mousedown listener obj.canvas.onmousedown = function (e) { if (e.button === 0 && obj.getXValue(e)) { var x = obj.getXValue(e); state.start = x; } }; // The mouseup listener. This is where the magic // happens and the chart is updated. obj.canvas.onmouseup = function (e) { if (e.button === 0) { var x = obj.getXValue(e), min = Math.min(state.start, x), max = Math.max(state.start, x); state.end = x; obj.set({ xaxisScaleMin: min, xaxisScaleMax: max }); obj.data[0].forEach(function (v, k, arr) { if (v[0] < min || v[0] > max) { arr[k] = []; } }); obj.set({ contextmenu: [['Reset', function () { obj.set({ xaxisScaleMin: original.xaxisScaleMin, xaxisScaleMax: original.xaxisScaleMax, xaxisLabels: RGraph.arrayClone(opt.options.xaxisLabels, true) }); obj.data[0] = RGraph.arrayClone(opt.data, true); RGraph.redraw(); }]] }); // Set the labels var newlabels = []; opt.options.xaxisLabels.forEach(function (v, k, arr) { if (v[1] > min && v[1] < max) { newlabels.push(v); } }); obj.set('xaxisLabels', newlabels); RGraph.redraw(); state = {}; } }; // The mousemove handler. This simply highlights from // the mousedown start point to the current position. obj.canvas.onmousemove = function (e) { if (typeof state.start === 'number' && !state.end) { var coordX1 = obj.getXCoord(state.start); var coordX2 = obj.getXCoord(obj.getXValue(e)); RGraph.redraw(); obj.context.fillStyle = 'rgba(0,0,0,0.15)'; obj.context.fillRect( coordX1, obj.properties.marginTop, coordX2 - coordX1, obj.canvas.height - obj.properties.marginTop - obj.properties.marginBottom ); } }; return obj; }; // // This is a place holder - it doesn't do anything // RGraph.Scatter.drilldown.draw = function (options) { return RGraph.Scatter.drilldown(options); }; // // This is a function that facilitates creating a // Marimekko chart. // RGraph.Scatter.Marimekko = function (opt) { this.options = opt.options; this.data = opt.data; this.id = opt.id; // // The draw function for the Marimekko chart // this.draw = function () { return new RGraph.Scatter({ id: this.id, data: opt.data, options: { xaxisScaleMax: 100, yaxisScaleMax: 100, backgroundGrid: false, xaxis: false, yaxis: false, yaxisScaleUnitsPost: '%', ...this.options } }).exec(function (obj) { // Set this flag so that we can tell if this // is a marimekko chart obj.isMarimekko = true; }).draw(); }; }; // // This is a "wrapper" function that creaters a dual-color // trendline Scatter 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 Scatter 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.Scatter.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.Scatter({ 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.Scatter({ 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-scatter-dual-color-effect-callback', function () { args.animationEffectCallback ? args.animationEffectCallback() : function () {}; }); } obj1[effect](effectOptions); obj2[effect](effectOptions, effectCallback); } else { RGraph.redraw(); } return [obj1, obj2]; };