// 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); // Convert objects to arrays for (let i=0; i 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= 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 0) prevY = this.coords[i][j - 1][1]; currY = yPos; if (j == 0 || RGraph.isNull(prevY) || RGraph.isNull(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 0) { var i=0; for (var set=0; set 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= (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.isNull(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 (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); // Point highlight } else { 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; } }; // // 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) { var data = RGraph.isArray(properties.bubbleData) && RGraph.isArray(properties.bubbleData[dataset]) ? properties.bubbleData[dataset] : properties.bubbleData; var min = RGraph.isArray(properties.bubbleMin) ? properties.bubbleMin[dataset] : properties.bubbleMin; var max = RGraph.isArray(properties.bubbleMax) ? properties.bubbleMax[dataset] : properties.bubbleMax; var width = RGraph.isArray(properties.bubbleWidth) ? properties.bubbleWidth[dataset] : properties.bubbleWidth; // Initialise the coordinates array this.coordsBubble[dataset] = []; // Loop through all the points (first dataset) for (var i=0; i 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=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); // Calculate the total of all the X values for (var i=0,totalX=0; i (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.isNull(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 max) { arr[k] = []; } }); obj.set({ contextmenu: [['Reset', function () { obj.set({ xaxisScaleMin: original.xaxisScaleMin, xaxisScaleMax: original.xaxisScaleMax, xaxisLabels: RGraph.arrayClone(opt.options.xaxisLabels) }); obj.data[0] = RGraph.arrayClone(opt.data); 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(); }; };