(function (factory) { if (typeof module === 'object' && typeof module.exports !== "undefined") { module.exports = factory; } else { factory(FusionCharts); } }(function (FusionCharts) { /**! * @license FusionCharts JavaScript Library - KDtree * Copyright FusionCharts Technologies LLP * License Information at * * @version 3.12.0 */ /** * @private * @module fusioncharts.renderer.javascript.kdtree * @export fusioncharts.kdtree.js */ FusionCharts.register('module', ['private', 'modules.renderer.js-kdtree', function () { /* * compareFunction : An optional argument for Array.Prototype.sort(). Used to change the default sorting order. * @param a {Object} and @param b {Object} - two elements being compared during the sort. */ // sorts the Array w.r.t to 'x' property of the array elements in an ascending order. var global = this, lib = global.hcLib, UNDEFINED, isWithinCircle = function (x, y, cx, cy, r) { return (Math.pow(cx - x, 2) + Math.pow(cy - y, 2) <= Math.pow(r, 2)); }, rad = function (deg) { return deg % 360 * deg2rad; }, mathPI = Math.PI, mathCos = Math.cos, mathSin = Math.sin, mathMax = Math.max, mathMin = Math.min, deg2rad = mathPI / 180, doIntersect = function (p1x, p1y, q1x, q1y, p2x, p2y, q2x, q2y) { // Find the four orientations needed for general and // special cases var o1 = orientation(p1x, p1y, q1x, q1y, p2x, p2y), o2 = orientation(p1x, p1y, q1x, q1y, q2x, q2y), o3 = orientation(p2x, p2y, q2x, q2y, p1x, p1y), o4 = orientation(p2x, p2y, q2x, q2y, q1x, q1y); // General case if (o1 !== o2 && o3 !== o4) { return true; } // Special Cases // p1, q1 and p2 are colinear and p2 lies on segment p1q1 if (o1 === 0 && onSegment(p1x, p1y, p2x, p2y, q1x, q1y)) { return true; } // p1, q1 and p2 are colinear and q2 lies on segment p1q1 if (o2 === 0 && onSegment(p1x, p1y, q2x, q2y, q1x, q1y)) { return true; } // p2, q2 and p1 are colinear and p1 lies on segment p2q2 if (o3 === 0 && onSegment(p2x, p2y, p1x, p1y, q2x, q2y)) { return true; } // p2, q2 and q1 are colinear and q1 lies on segment p2q2 if (o4 === 0 && onSegment(p2x, p2y, q1x, q1y, q2x, q2y)) { return true; } return false; // Doesn't fall in any of the above cases }, onSegment = function (p1x, p1y, q1x, q1y, p2x, p2y) { if (q1x <= mathMax(p1x, p2x) && q1x >= mathMin(p1x, p2x) && q1y <= mathMax(p1y, p2y) && q1y >= mathMin(p1y, p2y)) { return true; } return false; }, orientation = function (p1x, p1y, q1x, q1y, p2x, p2y) { var A = q1y - p1y, B = p2x - q1x, C = q1x - p1x, D = p2y - q1y, val1, val2, val; val1 = A * B; val2 = C * D; if (isNaN(val1)) { val1 = 0; } if (isNaN(val2)) { val2 = 0; } val = val1 - val2; if (val === 0) { return 0; // colinear } return (val > 0) ? 1: 2; }, isWithinPolygon = function (x, y, cx, cy, r, sides, startAngle) { var i, extreme = Infinity, angle, count = 0, p1x, p2x, p2y, p1y, inangle, inside = false; if (isWithinCircle(x, y, cx, cy, r) && sides >= 3) { angle = startAngle === UNDEFINED ? mathPI * 0.5 : rad(startAngle); inangle = 2 * mathPI / sides; p1x = cx + r * mathCos(-angle); p1y = cy + r * mathSin(-angle); for (i = 0; i < sides; i++) { angle += inangle; p2x = cx + r * mathCos(-angle); p2y = cy + r * mathSin(-angle); if (doIntersect(p1x, p1y, p2x, p2y, x, y, extreme, y)) { if (orientation(p1x, p1y, x, y, p2x, p2y) === 0) { return onSegment(p1x, p1y, x, y, p2x, p2y); } count++; } p1x = p2x; p1y = p2y; } inside = count % 2 !== 0; } return inside; }; function kdTreeAbs (arr) { 'use strict'; // Max radius will be tolerance var tolerance = arr && arr[0] && arr[0].r || 5, i, max = Math.max, floor = Math.floor, sqrt = Math.sqrt, min = Math.min, log = Math.log, exp = Math.exp, pow = Math.pow; arr = arr || []; // Find tolerance as the max radius // of the element for (i = arr.length; i--;) { if (arr[i].r > tolerance) { tolerance = arr[i].r; } // Setting the index arr[i].i = i; arr[i].x = +arr[i].x; arr[i].y = +arr[i].y; } // KdTree Definition below function buildKdTree(arr, left, right, isY){ var ob = {}, mid, access = isY ? 'y' : 'x'; if(left === right){ ob.point = arr[left]; return ob; } if(right - left === 1){ if(arr[left][access] > arr[right][access]){ ob.point = arr[left]; ob.left = { point: arr[right] }; } else { ob.point = arr[right]; ob.left = { point: arr[left] }; } return ob; } mid = (left + right) >> 1; if (isY) { quickselectY(arr, mid, left, right); } else { quickselectX(arr, mid, left, right); } ob.point = arr[mid]; ob.left = buildKdTree(arr, left, mid - 1, !isY); ob.right = buildKdTree(arr, mid + 1, right, !isY); return ob; } function quickselectX(arr, k, left, right) { var n, m, z, s, sd, newLeft, newRight, t, i, j; while (right > left) { if (right - left > 600) { n = right - left + 1, m = k - left + 1, z = log(n), s = 0.5 * exp(2 * z / 3), sd = 0.5 * sqrt(z * s * (n - s) / n) * (m - n / 2 < 0 ? -1 : 1), newLeft = max(left, floor(k - m * s / n + sd)), newRight = min(right, floor(k + (n - m) * s / n + sd)); quickselectX(arr, k, newLeft, newRight); } t = arr[k]; i = left; j = right; swap(arr, left, k); if (arr[right].x > t.x) { swap(arr, left, right); } while (i < j) { swap(arr, i, j); i++; j--; while (arr[i].x < t.x) { i++; } while (arr[j].x > t.x) { j--; } } if (arr[left].x === t.x) { swap(arr, left, j); } else { j++; swap(arr, j, right); } if (j <= k) { left = j + 1; } if (k <= j) { right = j - 1; } } } function quickselectY(arr, k, left, right) { var n, m, z, s, sd, newLeft, newRight, t, i, j; while (right > left) { if (right - left > 600) { n = right - left + 1, m = k - left + 1, z = log(n), s = 0.5 * exp(2 * z / 3), sd = 0.5 * sqrt(z * s * (n - s) / n) * (m - n / 2 < 0 ? -1 : 1), newLeft = max(left, floor(k - m * s / n + sd)), newRight = min(right, floor(k + (n - m) * s / n + sd)); quickselectY(arr, k, newLeft, newRight); } t = arr[k]; i = left; j = right; swap(arr, left, k); if (arr[right].y > t.y) { swap(arr, left, right); } while (i < j) { swap(arr, i, j); i++; j--; while (arr[i].y < t.y) { i++; } while (arr[j].y > t.y) { j--; } } if (arr[left].y === t.y) { swap(arr, left, j); } else { j++; swap(arr, j, right); } if (j <= k) { left = j + 1; } if (k <= j) { right = j - 1; } } } function swap(arr, i, j) { var tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; } return { tree: (arr.length === 0) ? {} : buildKdTree(arr, 0, arr.length - 1, false), search: function (x, y, type) { // Check a point is in range w.r.t // to given range function inRange (a, b) { if (type === 'circle') { return calcDist(a, b, x, y) <= x1; } return a >= x1 && a <= x2 && b >= y1 && b <= y2; } function inRangeApply (a, r1, r2) { return a >= r1 && a <= r2; } // Helper function for search // to apply data if found function apply (ob) { var currentHovered = inRangeApply(x, ob.x1, ob.x2) && inRangeApply(y, ob.y1, ob.y2), currentDist = calcDist(x, y, ob.point.x, ob.point.y); if (!res) { res = ob; lastHovered = currentHovered; lastDist = currentDist; return; } if (currentHovered) { if (lastHovered) { if (ob.point.i > res.point.i) { res = ob; lastHovered = currentHovered; lastDist = currentDist; } } else { res = ob; lastHovered = currentHovered; lastDist = currentDist; } } else { if (!lastHovered) { if (currentDist < lastDist) { res = ob; lastHovered = currentHovered; lastDist = currentDist; } } } } function defineSearchArea (r) { if (type === 'circle') { // Defining box again x1 = r; } else { x1 = x - r || 0; x2 = x + r || 0; y1 = y - r || 0; y2 = y + r || 0; } } // Calculate distance between two points function calcDist (x, y, p, q) { return sqrt(pow(x - p, 2) + pow(y - q, 2)); } // X and Y searching different for // maintaing performance function searchX (ob) { // Not found if (!ob || !ob.point) { return; } // defining search area defineSearchArea(ob.point.r); // If match found return if (inRange(ob.point.x, ob.point.y)) { apply(ob); } // If smaller x1 go left if (x1 <= ob.point.x) { searchY(ob.left); } // If bigger x2 goto right if (x2 >= ob.point.x) { searchY(ob.right); } } function searchY (ob) { // Not found if (!ob || !ob.point) { return; } // defining search area defineSearchArea(ob.point.r); // If match found return if (inRange(ob.point.x, ob.point.y)) { apply(ob); } // If smaller x1 go left if (y1 <= ob.point.y) { searchX(ob.left); } // If bigger x2 goto right if (y2 >= ob.point.y) { searchX(ob.right); } } // Actual search logic var tree = this.tree, res, x1 = x - tolerance, x2 = x + tolerance, y1 = y - tolerance, y2 = y + tolerance, lastHovered = false, lastDist = 0; searchX(tree); // Return point otherwise undefined value return res && res.point || res; } }; } /* * The entire data of the dataset is converted to a tree like structure(namely kdTree). * This is very useful to traverse and extract details(e.g. for a tooltip) from a million array elements especially * to extract information of the hovered plot on each mouseMove. * @constructor */ function KdTree (shape) { var kdTreeObj = this; kdTreeObj.configure(shape); } KdTree.prototype = { configure: function (shape) { var kdTreeObj = this; kdTreeObj.validatorFn = shape ? kdTreeObj.shapeValidator() : kdTreeObj.defaultValidator(); }, /* * Check Function: To determine if the mouseMove Point and the point to be searched are within limits * @param mousePoint {Object} - point extracted from the mouseMove. * @return {Function} - Returns if the setElem is in limit wrt the mouseMoved point. */ defaultValidator: function () { var kdTreeObj = this; return function (setElem) { var mousePoint = kdTreeObj.mousePoint; //Refer to the 2-D distance formula for two points // @returns {Boolean} - If the calculated distances are within the limits. return setElem && (Math.pow((setElem.x - mousePoint.x) / kdTreeObj.xLimit, 2) + Math.pow((setElem.y - mousePoint.y) / kdTreeObj.yLimit, 2) <= 1) ? true : false; }; }, shapeValidator: function () { var kdTreeObj = this; return function (setElem) { var mousePoint = kdTreeObj.mousePoint, shapeInfo = setElem && setElem.shapeInfo, inside = false, type, innerradius, r, x = mousePoint.x, y = mousePoint.y, sides, startAngle, x1, x2, y1, y2; type = shapeInfo && shapeInfo.type; switch (type) { case 'circle': r = shapeInfo.radius; inside = isWithinCircle(x, y, setElem.x, setElem.y, r); break; case 'arc': innerradius = shapeInfo.innerradius; r = shapeInfo.radius; inside = !isWithinCircle(x, y, setElem.x, setElem.y, innerradius) && isWithinCircle(x, y, setElem.x, setElem.y, r); break; case 'polygon': r = shapeInfo.radius; startAngle = shapeInfo.startAngle; sides = shapeInfo.sides; inside = isWithinPolygon(x, y, setElem.x, setElem.y, r, sides, startAngle); break; case 'rect': x1 = setElem.x; y1 = setElem.y; x2 = x1 + (shapeInfo.width || 0); y2 = y1 + (shapeInfo.height || 0); inside = x >= x1 && x <= x2 && y >= y1 && y <= y2; break; case 'default': inside = false; break; } return inside; }; }, /* * Builds the tree structure from the data points provided. * @param points {Array} - The set level elements belonging to a particular dataset * Updates the tree structure in the tree property of the current instance. */ buildKdTree: function (points) { /* * _buildKdTree is for the internal use to build the tree recursively. * @param points {Array} - The set level elements. * @param isX {Boolean} - isX determines the sorting order. Initially setting the isX = false ensures that * the initial sorting order of the array is w.r.t to y - axis */ this.kdTree = kdTreeAbs(points); this.tree = this.kdTree.tree; return this; }, /* * Returns the nearest point to the point given. * @param point {Object} - the point of reference in whose neighbour the point required is to be returned * @return {Object} - The neighbouring point w.r.t to the point is hunted and returned once within limits */ getNeighbour : function (point, basicSearch, type) { var kDTreeObj = this, // Instance of the kdTree class specific to indivual dataset instance. tree = kDTreeObj.tree, // refer to the created tree like structure for that dataset instance. // limitBox - set the limiting boundaries for the neighbour hunt of the given point. limitBox = { x1: point.x - kDTreeObj.xLimit, x2: point.x + kDTreeObj.xLimit, y1: point.y - kDTreeObj.yLimit, y2: point.y + kDTreeObj.yLimit }, //function specific to a paricular point that retruns Boolean if the neighbour searched is within limits validatorFn = kDTreeObj.validatorFn; kDTreeObj.mousePoint = point; // If basic search is on will use the search function from the // abstract implementation of kdTree if (basicSearch) { return this.kdTree && this.kdTree.search(point.x, point.y, type); } //When the renderring turns heavy, probably then the tree is not yet build. // Proceed further only if the tree exists. if (tree) { // search the nearest points within limits and return it. return kDTreeObj._searchBtwnLimit(limitBox, tree, true, validatorFn); } }, /* * If two points are very closely spreaded, the point that was parsed later is given preference and returned hence. * This is important when two plots are very near, the tooltip and hover effects of the one that comes avobe is being displayed * @param p1 {Object} - The first point being compared. * @param p2 {Object} - The second point being compared in close vicinity of the first point (p1). * @return {Object} - The point that occurs later in the JSON data gets the preference. */ _compair2closest: function (p1, p2) { if (!p1 || p1 && p2 && p2.i > p1.i) { return p2; } return p1; }, /* * Search and return the nearest neighbouring point in the pre defined limits. * @param limitBox {Object} - Boundary conditions for the tolerance for the neighbourhood search. * @param tree {Object} - The tree within which the point is needed to be searched, * @param isX {Boolean} - Flag representative of the axis. This gets swapped everytime the function is called recursively * @param validatorFn {Function} - Returns if the point is within the limits. * @return returnPoint {Object} - neighbouring point. */ _searchBtwnLimit : function(limitBox, tree, isX, validatorFn) { var point, returnPoint, kdTreeObj = this, axis = isX ? 'x': 'y', // get the lower and upper limits in accordance to the axis being referenced. lowerLimit = isX ? limitBox.x1 : limitBox.y1, upperLimit = isX ? limitBox.x2 : limitBox.y2; //Proceed further only when tree is defined point = tree && tree.point && tree.point[axis]; if (point === undefined) { return; } // point1 is within limit; if (validatorFn(tree.point)) { //In case of two closely spaced points, they are chosen preferably using _compair2closest() returnPoint = tree.point; } // search the left side if required if (point >= lowerLimit && tree.left) { // if left is available then search on left returnPoint = kdTreeObj._compair2closest(returnPoint, kdTreeObj._searchBtwnLimit(limitBox, tree.left, !isX, validatorFn)); } // search the right side if required if (point <= upperLimit && tree.right) { // if left is available then search on left returnPoint = kdTreeObj._compair2closest(returnPoint, kdTreeObj._searchBtwnLimit(limitBox, tree.right, !isX, validatorFn)); } return returnPoint; }, /* * Sets the tolerance limit for kdTree search after every zoom level * @param xLimit {Number} - The limiting tolerance in the x-axis * @param yLimit {Number} - The limiting tolerance in the y-axis */ _setSearchLimit : function (xLimit,yLimit) { var kdTree = this; //sets the x and y limits. kdTree.xLimit = xLimit; kdTree.yLimit = yLimit; } }; //reset the constructor KdTree.prototype.constructor = KdTree; lib.KdTree = KdTree; /* jshint ignore:start */ }]); /**! * @license FusionCharts JavaScript Library - ZoomScatter Chart * Copyright FusionCharts Technologies LLP * License Information at * * @version 3.12.0 */ /** * @private * @module fusioncharts.renderer.javascript.zoomscatter * @export fusioncharts.zoomscatter.js */ FusionCharts.register('module', ['private', 'modules.renderer.js-zoomscatter', function () { /*jslint newcap: false */ var global = this, lib = global.hcLib, parseTooltext = lib.parseTooltext, // extend2 = lib.extend2, // getLinkAction = lib.getLinkAction, // NumberFormatter = lib.NumberFormatter, R = lib.Raphael, win = global.window, addEvent = lib.addEvent, removeEvent = lib.removeEvent, MouseEvent = win.MouseEvent, doc = win.document, //strings BLANKSTRING = lib.BLANKSTRING, // HASHSTRING = lib.HASHSTRING, // getSentenceCase = lib.getSentenceCase, // dropHash = lib.regex.dropHash, //add the tools thats are requared pluck = lib.pluck, // getValidValue = lib.getValidValue, pluckNumber = lib.pluckNumber, getFirstValue = lib.getFirstValue, // parseUnsafeString = lib.parseUnsafeString, // FC_CONFIG_STRING = lib.FC_CONFIG_STRING, // getDashStyle = lib.getDashStyle, // returns dashed style of a line series toRaphaelColor = lib.toRaphaelColor, // toPrecision = lib.toPrecision, hasSVG = lib.hasSVG, isIE = lib.isIE || lib.isIE11, getFirstColor = lib.getFirstColor, hasTouch = lib.hasTouch, /*pInt = function(s, mag) { return parseInt(s, mag || 10); },*/ // The default value for stroke-dash attribute. // DASH_DEF = 'none', TRACKER_FILL = 'rgba(192,192,192,' + (isIE ? 0.002 : 0.000001) + ')', // invisible but clickable // M = 'M', math = Math, // mathSin = math.sin, // mathCos = math.cos, mathRound = math.round, mathMin = math.min, mathMax = math.max, mathAbs = math.abs, mathCos = math.cos, mathSin = math.sin, mathSqrt = math.sqrt, mathPow = math.pow, // mathCeil = math.ceil, mathFloor = math.floor, pi = Math.PI, pi2 = 2 * pi, getMouseCoordinate = lib.getMouseCoordinate, // getFirstColor = lib.getFirstColor, // setLineHeight = lib.setLineHeight, // pluckFontSize = lib.pluckFontSize, // To get the valid font size (filters negative values) // getDarkColor = lib.graphics.getDarkColor, // getLightColor = lib.graphics.getLightColor, // convertColor = lib.graphics.convertColor, POSITION_BOTTOM = lib.POSITION_BOTTOM, // POSITION_RIGHT = lib.POSITION_RIGHT, chartAPI = lib.chartAPI, // renderer = chartAPI, // ZEROSTRING = lib.ZEROSTRING, // ONESTRING = lib.ONESTRING, HUNDREDSTRING = lib.HUNDREDSTRING, // PXSTRING = lib.PXSTRING, // COMMASTRING = lib.COMMASTRING, creditLabel = false && !lib.CREDIT_REGEX.test(win.location.hostname), COLOR_WHITE = '#FFFFFF', COLOR_E3E3E3 = '#E3E3E3', COLOR_C2C2C2 = '#C2C2C2', COLOR_EFEFEF = '#EFEFEF', COLOR_E6E6E6 = '#E6E6E6', POINTER = 'pointer', //gradient colors used for the toogle button. // COLOR_ZOOM_TOOGLE = '0-#E6E6E6-#E6E6E6:49-#bfbfbf:49-#bfbfbf:51-#FFFFFF:51-#FFFFFF', // COLOR_PAN_TOOGLE = '0-#FFFFFF-#FFFFFF:49-#bfbfbf:49-#bfbfbf:51-#E6E6E6:51-#E6E6E6', //default cursor type. STR_DEF = 'default', //todo place as a local variable. // componentDispose = lib.componentDispose, /* CHeck if the mouseEvent was triggered within the canvas area and update its configurations wrt chart. * @param e {MouseEvent} - The original mouse event. * @param chart {Object} - The renderingAPI reference * @return The sanitized event adding the chartX, chartY property (pixel values wrt to chart specifically) and add an insideCanvas property to denote if the interaction was within the canvas area */ isWithinCanvas = function (e, chart) { var mousePos = getMouseCoordinate(chart.linkedItems.container,e), /*converts the original mouse event to a Fusion Charts event( that has chartX, chartY, pageX and pageY as its property)*/ chartX = mousePos.chartX, chartY = mousePos.chartY, chartConfig = chart.config, minX = chartConfig.canvasLeft, minY = chartConfig.canvasTop, maxX = chartConfig.canvasLeft + chartConfig.canvasWidth, maxY = chartConfig.canvasHeight + chartConfig.canvasTop; //default value of the flag. mousePos.insideCanvas = false; // store the original event as well mousePos.originalEvent = e; //return true if within the canvas if (chartX > minX && chartX < maxX && chartY > minY && chartY < maxY) { //set the flag to be TRUE if triggered within the canvas area. mousePos.insideCanvas = true; } return mousePos; }, // * Function to trigger a mouse event. // * @param eventName {String} - The name of the event to be triggered. // * @param domElement {HTML DOM element} - The element to be applied. // * @param mouseEventInit {MouseEvent} - Initial mouse event, whose replicate is needed to be fired. fireMouseEvent = function (eventName, domElement, mouseEventInit) { var event; if (!domElement || !eventName) { return; } if(!mouseEventInit){ mouseEventInit = {}; } if (mouseEventInit.originalEvent) { mouseEventInit = mouseEventInit.originalEvent; } // map touch event for touch devices if (mouseEventInit.touches) { mouseEventInit = mouseEventInit.touches[0]; } if (domElement.dispatchEvent) { if (MouseEvent && !isIE) { //for FireFox, chrome and opera. NOT confirmed in Safari // Creates a MouseEvent object. event = new MouseEvent(eventName, { bubbles: !!mouseEventInit.bubbles, cancelable: !!mouseEventInit.cancelable, clientX: mouseEventInit.clientX || ( mouseEventInit.pageX && (mouseEventInit.pageX - doc.body.scrollLeft - doc.documentElement.scrollLeft)) || 0, clientY: mouseEventInit.clientY || ( mouseEventInit.pageY && (mouseEventInit.pageY - doc.body.scrollTop - doc.documentElement.scrollTop)) || 0, screenX: mouseEventInit.screenX || 0, screenY: mouseEventInit.screenY || 0, pageX: mouseEventInit.pageX || 0, pageY: mouseEventInit.pageY || 0 }); } else if (doc.createEvent) { //for IE support. event = doc.createEvent('HTMLEvents'); event.initEvent(eventName, !!mouseEventInit.bubbles, !!mouseEventInit.cancelable); } event.eventName = eventName; event && domElement.dispatchEvent(event); } else if(doc.createEventObject && domElement.fireEvent){ event = doc.createEventObject(); event.eventType = eventName; event.eventName = eventName; //trigger the event forcefully. domElement.fireEvent('on' + eventName, event); } }, KdTree = lib.KdTree; chartAPI('zoomscatter', { standaloneInit: true, defaultDatasetType: 'zoomscatter', applicableDSList: { 'zoomscatter': true }, highlightEnabled: false, friendlyName: 'ZoomScatter Chart', isXY: true, defaultZeroPlaneHighlighted: false, creditLabel: creditLabel, configure: function () { var chartAttr, iapi = this, conf = iapi.config; chartAPI.scatter.configure.apply(iapi,arguments); chartAttr = iapi.jsonData.chart; conf.stepZoom = 1 - pluckNumber(chartAttr.stepzoom, 25) / 100; conf.showToolBarButtonToolText = pluckNumber(chartAttr.showtoolbarbuttontooltext, 1); conf.btnResetChartToolText = pluck(chartAttr.btnresetcharttooltext, 'Reset Chart'); conf.btnZoomOutToolText = pluck(chartAttr.btnzoomouttooltext, 'Zoom out to previous level'); conf.btnZoomInToolText = pluck(chartAttr.btnzoomintooltext, 'Zoom in
Or double-' + 'click on plot to zoom-in'); conf.btnSelectZoomToolText = pluck(chartAttr.btnselectzoomtooltext, 'Select a region to zoom-in
Click to enable pan mode.'); conf.btnPanToolText = pluck(chartAttr.btnpantooltext, 'Drag to move across chart
Click to enable select-zoom mode.'); }, _setAxisLimits : function () { var iapi = this, components = iapi.components, dataObj = iapi.jsonData, chartAttr = dataObj.chart, xAxis = components.xAxis, xAxisLabelMode = pluck (chartAttr.xaxislabelmode, 'categories'), categoriesArr = dataObj.categories && dataObj.categories[0], categories = (categoriesArr && categoriesArr.category || []).slice(), axisRange, increment, i, xMin = Infinity, xMax = -Infinity, catX; if (xAxisLabelMode === 'auto' || categories.length === 0) { chartAPI.mscartesian._setAxisLimits.call(iapi); axisRange = xAxis[0].getLimit(); increment = axisRange.tickInterval; xAxis[0].setAxisConfig({ hasCategory : 0 }); xAxis[0].resetCategoryAxisComponents(); // iterate through the categories to find the max and min of them. for (i = 0; i < categories.length; i += 1) { if (catX = categories[i].x) { if (catX < xMin) { xMin = catX; } if (catX > xMax) { xMax = catX; } } } // Update the axis Limits if the categories extremes exceeds them. if (xMax > axisRange.max || xMin < axisRange.min) { xMax = mathMax(xMax, axisRange.max); xMin = mathMin(xMin, axisRange.min); xAxis[0].setDataLimit(xMax, xMin); } } else { xAxis[0].resetNumericAxisComponents(); chartAPI.scatterBase._setAxisLimits.call(iapi); } }, _createAxes: function () { var iapi = this, components = iapi.components; chartAPI.scatter._createAxes.call(iapi, arguments); components.yAxis[0].setAxisConfig ( { animateAxis : false }); components.xAxis[0].setAxisConfig ( { animateAxis : false }); }, _spaceManager: function () { // todo marge _allocateSpace and _spacemanager var availableWidth, availableHeight, iapi = this, config = iapi.config, components = iapi.components, xAxis = components.xAxis && components.xAxis[0], yAxisArr = (components.yAxis && components.yAxis) || [], yAxis, len, legend = components.legend, legendPosition = legend.config.legendPos, xDepth = config.xDepth, yDepth = config.yDepth, canvasBgDepth = config.canvasBgDepth, allottedSpace, canvasBaseDepth = config.canvasBaseDepth, canvasBasePadding = config.canvasBasePadding, canvasBorderWidth = components.canvas.config.canvasBorderWidth, showRTValue = config.realTimeConfig && config.realTimeConfig.showRTValue, chartBorderWidth = config.borderWidth, canvasMarginTop = config.canvasMarginTop, canvasMarginBottom = config.canvasMarginBottom, canvasMarginLeft = config.canvasMarginLeft, canvasMarginRight = config.canvasMarginRight, minCanvasHeight = config.minCanvasHeight, minCanvasWidth = config.minCanvasWidth, minChartWidth = config.minChartWidth, minChartHeight = config.minChartHeight, height = config.height, width = config.width, diff, initConfig = config._initConfig || (config._initConfig = {}), heightAdjust = false, widthAdjust = false, top, bottom, left, right, i, currentCanvasHeight, origCanvasTopMargin = config.origCanvasTopMargin, origCanvasBottomMargin = config.origCanvasBottomMargin, chartBorderHorizontal, chartBorderVertical, canvasBorderHorizontal, canvasBorderVertical, yAxisSpaceAllocation = [], xAxisSpaceAllocation, spaceTakenByAxis, sum; initConfig.canvasLeft = config.canvasLeft; initConfig.canvasWidth = config.canvasWidth; initConfig.canvasHeight = config.canvasHeight; initConfig.canvasTop = config.canvasTop; initConfig.availableHeight = config.availableHeight; initConfig.availableWidth = config.availableWidth; if ((config.canvasWidth - 2 * chartBorderWidth) < minChartWidth ) { chartBorderVertical = (config.canvasWidth - minChartWidth) / 2; } if ((config.canvasHeight - 2 * chartBorderWidth) < minChartHeight ) { chartBorderHorizontal = (config.canvasHeight - minChartHeight) / 2; } iapi._allocateSpace ( { top : chartBorderHorizontal || chartBorderWidth, bottom : chartBorderHorizontal || chartBorderWidth, left : chartBorderVertical || chartBorderWidth, right : chartBorderVertical || chartBorderWidth }); //temp code.. for zoomscatter implementation. Once removed. Merge with scatter space manager. Delete this. config.origCanvasWidth = config.canvasWidth; config.origCanvasLeft = config.canvasLeft; config.canvasRight = config.canvasLeft + config.canvasWidth; config.origCanvasRight = config.canvasRight; //temp code ends. for (i = 0, len = yAxisArr.length; i < len; i++) { yAxis = yAxisArr[i]; //****** Manage space availableWidth = config.availableWidth * 0.7; spaceTakenByAxis = yAxis && yAxis.placeAxis(availableWidth) || {}; yAxisSpaceAllocation.push({ axisIndex: i, spaceTaken: spaceTakenByAxis }); yAxis && iapi._allocateSpace (spaceTakenByAxis); } availableHeight = config.canvasHeight * 0.7; iapi._manageActionBarSpace && iapi._allocateSpace(iapi._manageActionBarSpace(availableHeight)); //No space is allocated for legend drawing in single series charts iapi._manageLegendSpace(allottedSpace); availableHeight = (legendPosition === POSITION_BOTTOM) ? config.availableHeight * 0.6 : config.availableWidth * 0.6; //space management for 3d canvas if (yDepth) { iapi._allocateSpace ( { bottom : yDepth }); config.shift = xDepth + canvasBasePadding + canvasBaseDepth; } if (canvasBgDepth) { iapi._allocateSpace ( { right: canvasBgDepth }); } if ((config.canvasWidth - 2 * canvasBorderWidth) < minCanvasWidth ) { canvasBorderVertical = (config.canvasWidth - minCanvasWidth) / 2; } iapi._allocateSpace ( { left : canvasBorderVertical || canvasBorderWidth, right : canvasBorderVertical || canvasBorderWidth }); // Check for minimun canvas width for applying canvas left and right margin. if (minCanvasWidth > width - canvasMarginLeft - canvasMarginRight) { widthAdjust = true; diff = config.canvasWidth - minCanvasWidth; sum = canvasMarginLeft + canvasMarginRight; canvasMarginLeft = config.canvasMarginLeft = diff * canvasMarginLeft / sum; canvasMarginRight = config.canvasMarginRight = diff * canvasMarginRight / sum; } // Calculating the left and right canvas margin. left = canvasMarginLeft > config.canvasLeft ? (canvasMarginLeft - config.canvasLeft) : 0; right = canvasMarginRight > (width - config.canvasRight) ? (canvasMarginRight + config.canvasRight - width) : 0; iapi._allocateSpace ( { left : left, right : right }); availableHeight = (legendPosition === POSITION_BOTTOM) ? config.canvasHeight * 0.6 : config.canvasWidth *0.6; // a space manager that manages the space for the tools as well as the captions. iapi._manageChartMenuBar(availableHeight); availableHeight = config.availableHeight * 0.2; iapi._allocateSpace(iapi._getSumValueSpace(availableHeight)); availableHeight = config.availableHeight * 0.3; // temp code starts..... for zoomscatter implementation. config.origCanvasHeight = config.canvasHeight; config.canvasBottom = config.canvasTop + config.canvasHeight; config.origCanvasBottom = config.canvasBottom; config.origCanvasTop = config.canvasTop; //temp code ends... if (config.realtimeEnabled) { if (showRTValue) { iapi._allocateSpace(iapi._realTimeValuePositioning (availableHeight)); } else { iapi._hideRealTimeValue(); } } availableHeight = config.availableHeight * 0.6; config.xAxisSpaceAllocation = xAxisSpaceAllocation = xAxis && xAxis.placeAxis (availableHeight); xAxis && iapi._allocateSpace (xAxisSpaceAllocation); iapi._getDSspace && iapi._allocateSpace (iapi._getDSspace (config.canvasWidth * 0.4)); // alocate the space for scroll. availableHeight = config.availableHeight * 0.3; iapi._manageScrollerPosition && iapi._manageScrollerPosition(availableHeight); if ((config.canvasHeight - 2 * canvasBorderWidth) < minCanvasHeight ) { canvasBorderHorizontal = (config.canvasHeight - minCanvasHeight) / 2; } iapi._allocateSpace ( { top : canvasBorderHorizontal || canvasBorderWidth, bottom : canvasBorderHorizontal || canvasBorderWidth }); iapi._allocateSpace({ bottom: canvasBaseDepth }); // Check for minimum canvas height for applying top and bottom margin. if (minCanvasHeight > height - canvasMarginTop - canvasMarginBottom) { heightAdjust = true; diff = config.canvasHeight - minCanvasHeight; sum = canvasMarginTop + canvasMarginBottom; canvasMarginTop = config.canvasMarginTop = diff * canvasMarginTop / sum; canvasMarginBottom = config.canvasMarginBottom = diff * canvasMarginBottom / sum; } // Allocate space for canvas margin only if the margin is less than the margin entered by the user. top = canvasMarginTop > config.canvasTop ? (canvasMarginTop - config.canvasTop) : 0; bottom = canvasMarginBottom > (height - config.canvasBottom) ? (canvasMarginBottom + config.canvasBottom - height) : 0; iapi._allocateSpace ( { top : top, bottom : bottom }); // Forcing canvas height to its minimum if (heightAdjust) { sum = origCanvasTopMargin + origCanvasBottomMargin; currentCanvasHeight = config.canvasHeight; if (currentCanvasHeight > minCanvasHeight) { diff = currentCanvasHeight - minCanvasHeight; top = diff * origCanvasTopMargin / sum; bottom = diff * origCanvasBottomMargin / sum; } iapi._allocateSpace ( { top : top, bottom : bottom }); } config.actualCanvasMarginTop = top; config.actualCanvasMarginLeft = left; }, _createToolBox: function () { var toolBox, toolBoxGraphics, toolBoxAPI, group, SymbolStore, Symbol, zoomInButton, zoomOutButton, resetButton, toggleZoomInButton, togglePanButton, iapi = this, config = iapi.config, components = iapi.components, chartMenuBar = components.chartMenuBar, actionBar = components.actionBar, tooltip = config.showToolBarButtonToolText, stepZoom = config.stepZoom; /* Do not reconfigure the toolbox if its already drawn. This flag is set falsy on each time configurations are updated. */ if (chartMenuBar && chartMenuBar.drawn || actionBar && actionBar.drawn) { return; } chartAPI.mscartesian._createToolBox.call(iapi); toolBox = components.tb; toolBoxGraphics = toolBox.graphics || (toolBox.graphics = {}); toolBoxAPI = toolBox.getAPIInstances(toolBox.ALIGNMENT_HORIZONTAL); Symbol = toolBoxAPI.Symbol; SymbolStore = toolBoxAPI.SymbolStore; group = (components.chartMenuBar || components.actionBar).componentGroups[0], // Add the symbols in the chartMenuBar zoomInButton = toolBoxGraphics.zoomInButton = new Symbol('zoomInIcon', undefined, toolBox.idCount++, toolBox.pId) .attachEventHandlers({ 'click' : function () { iapi.zoom(stepZoom); }, 'tooltext': tooltip && config.btnZoomInToolText || BLANKSTRING }); zoomOutButton = toolBoxGraphics.zoomOutButton = new Symbol('zoomOutIcon', undefined, toolBox.idCount++, toolBox.pId) .attachEventHandlers({ 'click' : function () { var viewPortHistory = config.viewPortHistory, previousViewPortHist; //zoom out is possible only if it is not the initial unzoomed configurations. if (viewPortHistory.length > 1) { previousViewPortHist = viewPortHistory.slice(-2,-1)[0]; iapi.updateVisual(previousViewPortHist.x, previousViewPortHist.y, previousViewPortHist.scaleX, previousViewPortHist.scaleY); } }, 'tooltext': tooltip && config.btnZoomOutToolText || BLANKSTRING }); resetButton = toolBoxGraphics.resetButton = new Symbol('resetIcon', undefined, toolBox.idCount++, toolBox.pId) .attachEventHandlers({ 'click' : function () { var viewPortConfig = config.viewPortConfig, iapiGraphics = iapi.graphics; /*xAxis = components.xAxis[0], yAxis = components.yAxis[0];*/ if (config.viewPortHistory.length > 1) { viewPortConfig.isReset = true; iapi.zoomSelection(0, 0, config.canvasWidth, config.canvasHeight); //deletes all the configurations except the initial one. config.viewPortHistory.splice(1); iapiGraphics.trackerGroup.attr({ cursor: 'default' }); iapi.updateButtonVisual(); //reset the selection box after the chart is reset. iapi.updateSelectionBox(0,0,0,0); } global.raiseEvent('zoomReset', {}, iapi.chartInstance, [iapi.chartInstance.id]); }, 'tooltext': tooltip && config.btnResetChartToolText || BLANKSTRING }); toggleZoomInButton = toolBoxGraphics.toggleZoomInButton = new Symbol('zoomModeIcon', undefined, toolBox.idCount++, toolBox.pId).attachEventHandlers({ 'click' : function () { iapi.toogleDragPan(true); }, 'tooltext': tooltip && config.btnSelectZoomToolText || BLANKSTRING }); togglePanButton = toolBoxGraphics.togglePanButton = new Symbol('panModeIcon', undefined, toolBox.idCount++, toolBox.pId).attachEventHandlers({ 'click' : function () { iapi.toogleDragPan(true); }, 'tooltext': tooltip && config.btnPanToolText || BLANKSTRING }); group.addSymbol(togglePanButton, true); group.addSymbol(toggleZoomInButton, true); group.addSymbol(resetButton, true); group.addSymbol(zoomOutButton, true); group.addSymbol(zoomInButton, true); }, _createLayers: function () { var iapi = this, iapiGraphics = iapi.graphics, paper = iapi.components.paper; chartAPI.scatter._createLayers.call(iapi); //create the additional layers. //create the container element !iapiGraphics.imageContainer && (iapiGraphics.imageContainer = paper.group('dataset-orphan', iapiGraphics.datasetGroup)); iapi.__preDraw(); }, /* *calculate the 2-d distance between two given points(in pixels) * @param point1 {Object} - The first point to compute the 2-d distance * @param point2 {Object} - The second point to compute the 2-d distance * @return {NUmber} - 2-d distance between the points. */ _dist: function (point1, point2) { var chart = this, chartComponents = chart.components, dx, dy, xAxis = chartComponents.xAxis[0], yAxis = chartComponents.yAxis[0]; if (point1 && point2) { dx = (point1.x - point2.x) * xAxis.getPVR(); dy = (point1.y - point2.y) * yAxis.getPVR(); //point to point distance. return Math.sqrt(Math.pow(dx,2)+Math.pow(dy,2)); } }, __preDraw: function () { var iapi = this, iapiGraphics = iapi.graphics, imageContainer = iapiGraphics.imageContainer, iapiConfig = iapi.config, canvasLeft = iapiConfig.canvasLeft, canvasTop = iapiConfig.canvasTop, canvasWidth = iapiConfig.canvasWidth, canvasHeight = iapiConfig.canvasHeight, chartDef = iapi.jsonData.chart, linkedItems = iapi.linkedItems, listeners = linkedItems.eventListeners || (linkedItems.eventListeners = []), containerElem = iapi.linkedItems.container, paper = iapi.components.paper; iapiConfig.updateAnimDuration = 500; // default animation durations. //the origin of the container element should coincide with the origin(top-left) of the canvas area. imageContainer.transform('t' + canvasLeft + ',' + canvasTop); //apply clipping to the container element. imageContainer.attr({ 'clip-rect': canvasLeft + ',' + canvasTop + ',' + canvasWidth + ',' + canvasHeight }); iapiConfig.status = 'zoom'; //set the maximum scaleX and scaleY. iapiConfig.maxZoomLimit = pluckNumber(chartDef.maxzoomlimit, 1000); //stores the different visual configurations for a historical reference. iapiConfig.viewPortHistory = [{ scaleX: 1, scaleY: 1, x: 0, y: 0 }]; /*create a hot element of same size as that of the canvas area which is used to draw the tracker element for trendzone in axis module and also apply the cursor properties.*/ !iapiGraphics.trackerElem && (iapiGraphics.trackerElem = paper.rect(canvasLeft, canvasTop, canvasWidth, canvasHeight, 0, iapiGraphics.trackerGroup).attr({ 'fill': TRACKER_FILL, 'stroke': TRACKER_FILL })); //remove any existing events if any removeEvent(containerElem, hasTouch ? 'touchstart' : 'mousemove', iapi.searchMouseMove); //adds to event stack. listeners.push(addEvent(containerElem, 'touchstart mousemove', iapi.searchMouseMove, iapi)); //callbacks for the image is set iapi.zoomPanManager(imageContainer); }, /* * Searches for the nearest neighbouring point from the point being hovered(/ moved). * @param e {MouseEvent} - Mouse MOve event */ searchMouseMove: function (e) { var mousePos, top, left, point, chart = e.data, chartConfig = chart.config, viewPortConfig = chartConfig.viewPortConfig, chartComponents = chart.components, xAxis = chartComponents.xAxis[0], yAxis = chartComponents.yAxis[0], trendlines = chartComponents.trendlines, vTrendlines = chartComponents.vTrendlines, hoverPoint, hoveredTrendLine, hoveredVtrendLine; if (!chart.linkedItems.container || chartConfig.isDragging) { return ; } //check if the event is fired within the canvas region. if ((mousePos = isWithinCanvas(e, chart)) && mousePos.insideCanvas) { //Convert the event coordinates into chart coordinates top = chartConfig.canvasTop; left = xAxis.getAxisConfig('axisDimention').x || chartConfig.canvasLeft; //Get the coordinates of the point wrt inital unzoomed canvas origin(in pixels) //convert to x, y-values. point = viewPortConfig.lastMouseCoordinate = { 'x': Number(xAxis.getValue(mousePos.chartX-left)), 'y': Number(yAxis.getValue(mousePos.chartY-top)) }; // clear previous search timer //clearTimeout(viewPortConfig.neighbourSearchTimer); //store the evnt object. This will be used to generate event on same coordinate chartConfig.lastMouseEvent = e; // for performance do the search in a seperate thread viewPortConfig.neighbourSearchTimer = (function () { // this checking prevent the state where the timeout was called when the chart was not in idDragging // state. But when the when the timer is getting executed, the chart is in isDragging state if (!chartConfig.isDragging){ //search the best neighbouring point of the mouse moved point. hoverPoint = chart._bestNeighbour(viewPortConfig.lastMouseCoordinate); hoveredTrendLine = chart._getHoveredTrendLine(point, trendlines); hoveredVtrendLine = chart._getHoveredTrendLine(point, vTrendlines); if (hoveredTrendLine && hoveredTrendLine.showontop) { chart._drawTooltip(e.originalEvent, hoveredTrendLine.tooltext); } else if (!hoveredTrendLine && hoveredVtrendLine && !hoverPoint){ chart._drawTooltip(e.originalEvent, hoveredVtrendLine.tooltext); } else if (hoveredTrendLine && !hoveredVtrendLine && !hoverPoint){ chart._drawTooltip(e.originalEvent, hoveredTrendLine.tooltext); }else if (hoveredTrendLine && hoveredVtrendLine && !hoverPoint){ chart._drawTooltip(e.originalEvent, hoveredVtrendLine.tooltext); } } })(); } else { //hide the tracker element. //lib.toolTip.hide(); chart.highlightPoint(false); } }, /* * Creates a rectangular selection Box. * This gives a visual clue of what portion is being selected using the selectZoom option. * All the pixel informations are provided wrt to canvas area origin(top-left). * Store the selection box dimensions as a reference to the viewPortConfig Object. * @param x1 {Number} - x-coordinate of the left top point of the selection rectangle to be created * @param y1 {Number} - y-coordinate of the left top point of the selection rectangle to be created * @param x2 {Number} - x-coordinate of the right bottom point of the selection rectangle to be created * @param y2 {Number} - y-coordinate of the right bottom point of the selection rectangle to be created */ updateSelectionBox: function (x1,y1,x2,y2) { var chart = this, chartComponents = chart.components, chartConfig = chart.config, chartGraphics = chart.graphics, paper = chartComponents.paper, viewPortConfig = chartConfig.viewPortConfig, cursor = chartConfig.cursor, selectionBox = chartGraphics.selectionBox, canvasRight = chartConfig.canvasRight, canvasLeft = chartConfig.canvasLeft, canvasTop = chartConfig.canvasTop, canvasBottom = chartConfig.canvasBottom, smallerX = (x1 < x2) ? x1: x2, smallerY = (y1 < y2) ? y1 : y2, largerX = (x1 > x2) ? x1 : x2, largerY = (y1 > y2) ? y1 : y2, /*Width and Height are temporary used in determining the cursor mode and updated later*/ width = x2-x1, height = y1-y2; //change the cursor modes according to the width and height if (cursor) { if (width > 0 && height > 0) { cursor = 'ne-resize'; } else if (width < 0 && height > 0) { cursor = 'nw-resize'; } else if (width < 0 && height < 0){ cursor = 'sw-resize'; } else if (width > 0 && height < 0) { cursor = 'se-resize'; } //reset the cursor to default once the selection box is removed(atleast visually) //i.e. width and height = 0 else { cursor = STR_DEF; //default cursor. } } else{ cursor = STR_DEF; } //dimension for the selectionBox cannot be exceeded than the canvas area dimensions. largerX = (largerX > canvasRight) ? canvasRight : ((largerX < canvasLeft) ? canvasLeft : largerX); largerY = (largerY > canvasBottom) ? canvasBottom : ((largerY < canvasTop) ? canvasTop : largerY); smallerX = (smallerX < canvasLeft) ? canvasLeft : ((smallerX > canvasRight) ? canvasRight : smallerX); smallerY = (smallerY < canvasTop) ? canvasTop : ((smallerY > canvasBottom) ? canvasBottom : smallerY); width = (x1 === x2 && y1 === y2) ? 0 : (largerX - smallerX); height = (x1 === x2 && y1 === y2) ? 0 : (largerY - smallerY); if (selectionBox){ //update the dimensions and posiitons of the selection Box. selectionBox.attr({ x: smallerX, y: smallerY, width: width, height: height, cursor: cursor }); chartGraphics.trackerGroup.attr({ //update the cursor shape. cursor: cursor }); } //Create the selection Box. else{ //shouldnot be hardcoded. selectionBox = chartGraphics.selectionBox = paper.rect(smallerX,smallerY,width,height).attr({ 'stroke-width': 1, 'stroke': 'red', 'fill': '#00FF00', 'opacity': 0.2, 'cursor': cursor }); chartGraphics.trackerGroup.attr({ //update the cursor shape. cursor: cursor }); } chartConfig.cursor = cursor; //store the selection Box dimensions for future reference. if (!viewPortConfig.selectionDimensions) { viewPortConfig.selectionDimensions = { startX: 0, endX: 0, startY: 0, endY: 0 }; } //update the dimensions. viewPortConfig.selectionDimensions.startX = x1; viewPortConfig.selectionDimensions.endX = y1; viewPortConfig.selectionDimensions.startY = x2; viewPortConfig.selectionDimensions.endY = y2; }, /* * @param point {Object} - Pixel values in chartX,chartY at any scale of viewPortConfigurations. * @Retrun {Object} - Axes values corresponding to it. */ getValue: function (point) { var chart = this, chartConfig = chart.config, chartComponents = chart.components, viewPortConfig = chartConfig.viewPortConfig, //the pixel wrt original canvas size origpixel = chart.getOriginalPositions(point.x,point.y,point.x,point.y), origX = origpixel[0], origY = origpixel[1], xAxis = chartComponents.xAxis[0], yAxis = chartComponents.yAxis[0], xaxisRange = xAxis.config.axisRange, yaxisRange = yAxis.config.axisRange, minX = xaxisRange.min, maxX = xaxisRange.max, maxY = yaxisRange.max, minY = yaxisRange.min, //calcualte the Pixel to Value Ratios. xPVR = chartConfig.canvasWidth * viewPortConfig.scaleX / (maxX - minX), yPVR = chartConfig.canvasHeight * viewPortConfig.scaleY / (maxY - minY); return { x: minX + ((origX - chartConfig.canvasLeft) / xPVR), y: maxY - ((origY - chartConfig.canvasTop) / yPVR) }; }, /* * Get a rectangular configuration out of the multi point events. * @param startEventPoint {Object} - Contains the diagonal points of the startTouch Points in case of multiTouch * @param moveEventPoint {Object} - Contains the diagonal points of the touchMove Points in case of multiTouch. These diagonal points creates an extended version of that created by startEventPoint * @param initialViewPort {Object} - The initial viewPort configurations while the startEvent was being fired. * @return {Object} - A rectangular configuration which needs to be zoomed. */ _getTouchViewPort: function (startEventPoint, moveEventPoint, initialViewPort) { var maxX, maxY, chart = this, chartConfig = chart.config, left = chartConfig.canvasLeft, top = chartConfig.canvasTop, width = chartConfig.canvasWidth, height = chartConfig.canvasHeight, //initial rectangle xInitial = mathMin(startEventPoint[0].chartX , startEventPoint[1].chartX) - left, yInitial = mathMin(startEventPoint[0].chartY , startEventPoint[1].chartY) - top, wInitial = Math.abs(startEventPoint[1].chartX - startEventPoint[0].chartX), hInitial = Math.abs(startEventPoint[1].chartY - startEventPoint[0].chartY), //final Rectangle xFinal = mathMin(moveEventPoint[0].chartX, moveEventPoint[1].chartX) - left, yFinal = mathMin(moveEventPoint[0].chartY , moveEventPoint[1].chartY) - top, wFinal = Math.abs(moveEventPoint[1].chartX - moveEventPoint[0].chartX), hFinal = Math.abs(moveEventPoint[1].chartY - moveEventPoint[0].chartY), //get the new scale factors scaleNewX = initialViewPort.scaleX * (wFinal / wInitial), scaleNewY = initialViewPort.scaleY * (hFinal / hInitial), xNew = (initialViewPort.x + (xInitial / initialViewPort.scaleX)) - (xFinal / scaleNewX), yNew = (initialViewPort.y + (yInitial / initialViewPort.scaleY)) - (yFinal / scaleNewY); //restrictions for origin and scale configurations. xNew = xNew < 0 ? 0 : xNew; yNew = yNew < 0 ? 0 : yNew; scaleNewY = scaleNewY < 1 ? 1 : scaleNewY; scaleNewX = scaleNewX < 1 ? 1 : scaleNewX; maxX = width - (width / scaleNewX); maxY = height - (height / scaleNewY); xNew = xNew > maxX ? maxX : xNew; yNew = yNew > maxY ? maxY : yNew; // the rectangular configurations build on the two recieved rectangles. return { x: xNew, y: yNew, scaleX: scaleNewX, scaleY: scaleNewY }; }, /* * At any level of zoom and in a state of shifted origin, this function converts and return the pixels wrt the original unzoomed configurations for the rectangle provided. * Original configurations are useful for all the zoom and pan calculations of the pixels. * @param x1 {Number} - x coordinate of the left top point of the rectangle. * @param y1 {Number} - y coordinate of the right bottom point of the rectangle. * @param x2 {Number} - x coordinate of the left top point of the rectangle. * @param y2 {Number} - y coordinate of the right bottom point of the rectangle. * @return {Array} - The coordinates wrt original unzoomed conditions. */ getOriginalPositions: function (x1,y1,x2,y2) { var newW, newH, newX, newY, chart = this, chartConfig = chart.config, viewPortConfig = chartConfig.viewPortConfig, oldScaleX = viewPortConfig.scaleX, oldScaleY = viewPortConfig.scaleY, //coodinates of the visual canvas origin wrt to original canvas. oldX = viewPortConfig.x, oldY = viewPortConfig.y, xMin = mathMin(x1, x2), xMax = mathMax(x1, x2), yMin = mathMin(y1, y2), yMax = mathMax(y1, y2); //Right Bottom limit boundary xMax = xMax > chartConfig.canvasWidth ? chartConfig.canvasWidth : xMax; yMax = yMax > chartConfig.canvasHeight ? chartConfig.canvasHeight : yMax; //Left Top Limit Boundary xMin = xMin < 0 ? 0 : xMin; yMin = yMin < 0 ? 0 : yMin; // update the dimensions wrt to initial viewPort configurations. newW = (xMax - xMin ) / oldScaleX; newH = (yMax - yMin ) / oldScaleY; newX = oldX + (xMin / oldScaleX); newY = oldY + (yMin / oldScaleY); //converts to the coordinates wrt original image return [newX, newY, newW, newH]; }, /* * Impose restrictions and initiate the zooming effect. * This is useful when the rectangular area points are provided and needed to zoom that area. * @param x {Number} - x-coordinate of the top left origin of the rectangular area to be zoomed. * @param y {Number} - y-coordinate of the top left origin of the rectangular area to be zoomed. * @param w {Number} - width of the rectangular area to be zoomed. * @param h {Number} - height of the rectangular area to be zoomed. */ zoomSelection: function (x, y, w, h) { var chart = this, chartConfig = chart.config, scaleX, scaleY, newWidth, newHeight, maxX, maxY, newOriginX, newOriginY; // if the width or height is 0 return it if (!w || !h) { return; } //scale factors cannnot be negatives. scaleX = Math.abs(chartConfig.canvasWidth / w); scaleY = Math.abs(chartConfig.canvasHeight / h); //total dimensions it would look alike when zoomed with these scale factors newWidth = chartConfig.canvasWidth * scaleX; newHeight = chartConfig.canvasHeight * scaleY; //the amount to be shifted so that the zoom portion falls in the visible area. newOriginX = x * scaleX; newOriginY = y * scaleY; //impose restrictions on the boundaries. maxX = (newWidth - chartConfig.canvasWidth); maxY = (newHeight - chartConfig.canvasHeight); //left top restricition newOriginX = (newOriginX < 0) ? 0 : newOriginX; newOriginY = (newOriginY < 0) ? 0 : newOriginY; //right bottom restriction newOriginX = (newOriginX > maxX) ? maxX : newOriginX; newOriginY = (newOriginY > maxY) ? maxY : newOriginY; //update the final visual(drawing) part. chart.updateVisual(x, y, scaleX, scaleY); }, //Toogle the status from 'zoom' and 'pan' mode. toogleDragPan: function (isToogle) { var chart = this, chartConfig = chart.config, status = chartConfig.status; if (isToogle) { chartConfig.status = (status === 'zoom') ? 'pan': 'zoom'; global.raiseEvent('zoomModeChanged', { panModeActive: chartConfig.status }, chart.chartInstance, [chart.chartInstance.id]); } //visual update chart.updateButtonVisual(); }, /* * Manages and updates all the datasets' drawing. * Draws the quadrant if required. * @param pixelatedDraw {Boolean} - Flag to zoom in a pixelerated manner while the pinch zoom interaction is going on. */ updateManager: function (pixelatedDraw) { var i, chart = this, chartComponents = chart.components, dataSets = chartComponents.dataset, len = dataSets.length; // redraw the datasets for (i = 0; i < len; i += 1) { dataSets[i].draw(pixelatedDraw); } //draw the qudrants if required. if (!pixelatedDraw) { chart._drawQuadrant(); } }, /* * helps attaching the essential callbacks for mouse and touch interactions. */ zoomPanManager: function () { var touchPoint, //stroes the start event startEvent = [], current = [], chart = this, chartConfig = chart.config, chartGraphics = chart.graphics, chartComponents = chart.components, xAxis = chartComponents.xAxis[0], listeners = chartComponents.eventListeners || (chartComponents.eventListeners = []), viewPortConfig = chart.viewPortConfig, isMultiTouch = false, // flag for number of touches if its a single or multi touch. dblClick = 0, // to distinguish double and single click. firstClickIsMouse, initialViewPortConfig = {}, containerElem = chart.linkedItems.container, /* * Imposes restrictions on the chartX and chartY as it cannot exceed the canvas area dimensions * @param event {Object} - Fetches the event */ forceLimit = function (event) { var chartX = event.chartX, chartY = event.chartY, left = xAxis.getAxisConfig('axisDimention').x || chartConfig.canvasLeft, top = chartConfig.canvasTop, right = chartConfig.canvasRight, bottom = chartConfig.canvasBottom; (chartX < left) && (event.chartX = mathMax(chartX, left)); (chartX > right) && (event.chartX = mathMin(chartX, right)); (chartY < top) && (event.chartY = mathMax(chartY, top)); (chartY > bottom) && (event.chartY = mathMin(chartY, bottom)); }, dragCallbacks = { /* * @param event {Object} - Events for touchStart and dragStart events. */ start: function (event) { //update the viewPort configurations. viewPortConfig = chartConfig.viewPortConfig; /*event.touches tells if the interaction was done by a mouse or touch. Orelse in case of hybrid devices, although the hasTouch turns TRUE, it has confusions and error prone if the interaction was mouse based, because event.touches remains undefined in that situation */ hasTouch = (event.touches) ? true : false; //for mouseEvent or single touch event. startEvent[0] = isWithinCanvas(event, chart); // todo remove this fix once the drag-move is applied on page body (outside chart) // hide the zoom-selection box chart.updateSelectionBox(0,0,0,0); // check whether this is a multi touch if (hasTouch) { isMultiTouch = false; // default its a single touch. touchPoint = event.touches.length; if (touchPoint === 2) { // for pinch zoom isMultiTouch = true; // store the initial view port initialViewPortConfig.x = viewPortConfig.x; initialViewPortConfig.y = viewPortConfig.y; initialViewPortConfig.scaleX = viewPortConfig.scaleX; initialViewPortConfig.scaleY = viewPortConfig.scaleY; //store the default multi touched event startEvent[0] = isWithinCanvas(event.touches.item(0), chart); startEvent[1] = isWithinCanvas(event.touches.item(1), chart); } } // reset the dragging flag chartConfig.isDragging = false; // if in pan mode if (chartConfig.status === 'pan') { chartConfig.panStartX = viewPortConfig.x; chartConfig.panStartY = viewPortConfig.y; //update the cursor. startEvent[0].insideCanvas && (chartGraphics.trackerGroup.attr({ cursor: 'move' })); } }, /* * @param event {Object} - Events for touchMove and dragMove events. */ on: function (event){ var maxX, maxY, newHeight, newOriginX, newOriginY, newViewPortConfig, dx, dy, panStartX = chartConfig.panStartX, panStartY = chartConfig.panStartY, scaleX = viewPortConfig.scaleX, scaleY = viewPortConfig.scaleY, touchPoint, xAxis = chartComponents.xAxis[0], yAxis = chartComponents.yAxis[0]; // if the interaction was based on mouse or touch hasTouch = (event.touches) ? true : false; //for single touch or drag events current[0] = isWithinCanvas(event, chart); if (!current[0].insideCanvas) { chartGraphics.trackerGroup.attr({ cursor: STR_DEF }); } else { (chartConfig.status === 'pan') && chartGraphics.trackerGroup.attr({ cursor: 'move' }); } if (hasTouch && event) { touchPoint = event.touches.length; if (touchPoint === 2) { isMultiTouch = true; //set the current event for multi touch event current[0] = isWithinCanvas(event.touches.item(0), chart); current[1] = isWithinCanvas(event.touches.item(1), chart); } else { isMultiTouch = false; } } /*for touch devices, the mouse move may get called during non-moved tap if dragging flag is not enabled Not to detect that as a drag, don't count the mouse move untill it moves significently*/ if(!chartConfig.isDragging && (!hasTouch ||mathAbs(current[0].chartX - startEvent[0].chartX) > 2 || mathAbs(current[0].chartY - startEvent[0].chartY) > 2)) { // reset the dragging flag chartConfig.isDragging = true; //hide the tracker element. chart.highlightPoint(false); if (chartConfig.status === 'zoom') { //convert to axis value startEvent[0].pointValue = chart.getValue({ x: startEvent[0].chartX, y: startEvent[0].chartY }); /* * Raised when the user starts to draw a selection box on a zoomscatter chart. * * @param {number} chartX - The x-coordinate of the mouse with respect to the chart. * @param {number} chartY - The y-coordinate of the mouse with respect to the chart. * @param {number} pageX - The x-coordinate of the mouse with respect to the page. * @param {number} pageY - The y-coordinate of the mouse with respect to the page. * @param {number} startXValue - The value on the canvas x-axis where the selection started. * @param {number} startYValue - The value on the canvas y-axis where the selection started. * @param {number} selectionTop - Distance between the top edges of the selection box and the chart * @param {number} selectionLeft - Distance between the left edges of the selection box and the chart * @param {number} selectionWidth - Width of the selection box (0 for this event, because it is triggered before you finish drawing the box) * @param {number} selectionHeight - Height of the selection box (0 for this event, because it is triggered before you finish drawing the box) */ global.raiseEvent('selectionStart', { chartX: startEvent[0].chartX, chartY: startEvent[0].chartY, pageX: startEvent[0].pageX, pageY: startEvent[0].pageY, selectionTop: startEvent[0].chartX + chartConfig.canvasLeft, selectionLeft: startEvent[0].chartY + chartConfig.canvasTop, selectionWidth: 0, selectionHeight: 0, startXVAlue: startEvent[0].pointValue.x, startYVAlue: startEvent[0].pointValue.y }, chart.chartInstance, [chart.chartInstance.id]); } } // if this is dragging then do the drag end if (chartConfig.isDragging) { // do the multitouch zoom if (isMultiTouch) { if (startEvent[0].insideCanvas || startEvent[1].insideCanvas) { //get a rectangular configuration out of the multi point events. newViewPortConfig = chart._getTouchViewPort(startEvent, current, initialViewPortConfig); // zoom the newViewPortConfig rectangular configurations chart.updateVisual(newViewPortConfig.x, newViewPortConfig.y, newViewPortConfig.scaleX, newViewPortConfig.scaleY, true); } } else if (chartConfig.status === 'zoom'){ // do the zooming //update the size of the rectangle chart.updateSelectionBox(startEvent[0].chartX, startEvent[0].chartY, current[0].chartX, current[0].chartY); } else if (chartConfig.status === 'pan' && (scaleX !== 1 || scaleY !== 1) && startEvent[0].insideCanvas){ // do the paning maxX = chartConfig.canvasRight; maxY = chartConfig.canvasBottom; //calculate the incremental change. dx = startEvent && (current[0].chartX - startEvent[0].chartX); dy = startEvent && (current[0].chartY - startEvent[0].chartY); panStartX -= dx / scaleX; panStartY -= dy / scaleY; maxX = (chartConfig.canvasWidth * (scaleX - 1))/scaleX; maxY = (chartConfig.canvasHeight * (scaleY - 1))/scaleY; panStartX = panStartX > maxX ? maxX: (panStartX < 0 ? 0 : panStartX); panStartY = panStartY > maxY ? maxY : (panStartY < 0 ? 0 : panStartY); viewPortConfig.x = panStartX; viewPortConfig.y = panStartY; //updates(redraw) the plot as per panning interactions. chart.updateManager(); newHeight = chartConfig.canvasHeight * scaleY; newOriginX = viewPortConfig.x * scaleX; newOriginY = viewPortConfig.y * scaleY; //Redraw the axes on zooming xAxis.draw(); yAxis.draw(); } } }, /* * @param event {Object} - Events for touchEnd and dragEnd events. */ end: function (event) { var mousePos = isWithinCanvas(event, chart), left = xAxis.getAxisConfig('axisDimention').x || chartConfig.canvasLeft, top = chartConfig.canvasTop, canvasWidth = chartConfig.canvasWidth, canvasHeight = chartConfig.canvasHeight, oriPositions, selectionEnd, selectionStart, newViewPortConfig, quaterCanvasW = canvasWidth / 4, quaterCanvasH = canvasHeight / 4, mouseX, mouseY, searchEvent; // if the mouse pointer is not valid, retrive the mouse pointer from last mousemove. mousePos = (isNaN(mousePos.chartX) || isNaN(mousePos.chartY)) ? (current[0] || startEvent[0]) : mousePos; // if this is dragging then do the drag end if (chartConfig.isDragging) { if (isMultiTouch) { // if multi touch // at the touch end we don't need to do the scale calculations again // we just have to redraw the canvas if (startEvent[0].insideCanvas || startEvent[1].insideCanvas) { //get a rectangular configuration out of the multi point events. newViewPortConfig = chart._getTouchViewPort(startEvent, current, initialViewPortConfig); // zoom the newViewPortConfig rectangular configurations chart.updateVisual(newViewPortConfig.x, newViewPortConfig.y, newViewPortConfig.scaleX, newViewPortConfig.scaleY); } //reset the flag. isMultiTouch = false; } // for single point drag in select zoom state else if (chartConfig.status === 'zoom'){ //delete the selection box and call for update plot selectionEnd = mousePos; selectionStart = startEvent[0]; // limit the actions within the canvas area only forceLimit(selectionStart); forceLimit(selectionEnd); //get the original positions of the selection events wrt original unzoomed canvas area. oriPositions = chart.getOriginalPositions( selectionStart.chartX - left, selectionStart.chartY - top, selectionEnd.chartX - left, selectionEnd.chartY - top); //reset only if it was a drag mouse event if (oriPositions){ // get the axis values of the end event. selectionEnd.pointValue = chart.getValue({ x: selectionEnd.chartX, y: selectionEnd.chartY }); /* * Triggered when you complete drawing a selection box on a zoomScatter chart. * * @param {number} chartX - x-coordinate of the pointer, relative to the chart * @param {number} chartY - y-coordinate of the pointer, relative to the chart * @param {number} pageX - x-coordinate of the pointer, relative to the page * @param {number} pageY - y-coordinate of the pointer, relative to the page * @param {number} startXValue - Value of the starting position of the selection box, in the canvas x-axis * @param {number} startYValue - Value of the starting position of the selection box, in the canvas y-axis * @param {number} endXValue - Value of the ending position of the selection box, in the canvas x-axis * @param {number} endYValue - Value of the ending position of the selection box, in the canvas y-axis * @param {number} selectionTop - Distance between the top edges of the selection box and the chart * @param {number} selectionLeft -Distance between the left edges of the selection box and the chart * @param {number} selectionWidth - Width of the selection box * @param {number} selectionHeight - Height of the selection box */ global.raiseEvent('selectionEnd', { chartX: selectionEnd.chartX, chartY: selectionEnd.chartY, pageX: selectionEnd.pageX, pageY: selectionEnd.pageY, selectionTop: selectionEnd.chartX + chartConfig.canvasLeft, selectionLeft: selectionEnd.chartY + chartConfig.canvasTop, selectionWidth: Math.abs(selectionEnd.chartX - selectionStart.chartX), selectionHeight: Math.abs(selectionEnd.chartY - selectionStart.chartY), startXVAlue: selectionStart.pointValue.x, startYVAlue: selectionStart.pointValue.y, endXValue: selectionEnd.pointValue.x, endYValue: selectionEnd.pointValue.y }, chart.chartInstance, [chart.chartInstance.id]); //when the drag start and end both happens outside the canvas area. if (selectionStart.chartX !== selectionEnd.chartX && selectionStart.chartY !== selectionEnd.chartY){ chart.zoomSelection(oriPositions[0], oriPositions[1], oriPositions[2], oriPositions[3]); } // hide the zoom-selection box chart.updateSelectionBox(0,0,0,0); } } // reset the is dragging flag chartConfig.isDragging = false; // call the mouse move point search function at the drag end searchEvent = current[0] || startEvent[0]; //store the reference of the rendering API in the event itself searchEvent.data = chart; chart.searchMouseMove(searchEvent); } else if(mousePos.insideCanvas){// detect as a mouse click inside canvas dblClick += 1; //doubeTouch flag turns false if no click occurs in 500ms. setTimeout(function () { dblClick = 0; firstClickIsMouse = undefined; },500); if (dblClick === 2 ){ if(((event && event.touches) ? true : false) === firstClickIsMouse) { // detect double click mouseX = mousePos.chartX - left; mouseY = mousePos.chartY - top; oriPositions = chart.getOriginalPositions(mouseX - quaterCanvasW, mouseY - quaterCanvasH, mouseX + quaterCanvasW, mouseY + quaterCanvasH); chart.zoomSelection(oriPositions[0], oriPositions[1], oriPositions[2], oriPositions[3]); event.preventDefault(); } } else {// detect as click firstClickIsMouse = (event && event.touches) ? true : false; } } // reset the coursor if any applied chartGraphics.trackerGroup.attr({ cursor: STR_DEF }); } }; //todo remove event and remove the annonomisity of the callback function with removeEvent. //callback for the interactions listeners.push(addEvent(containerElem, 'pointerdrag', function (evt) { dragCallbacks[evt.state](evt.originalEvent); })); }, _drawDataset: function () { var i, iapi = this, chartComponents = iapi.components, dataSets = chartComponents.dataset, len = dataSets.length; // redraw the datasets for (i = 0; i < len; i += 1) { dataSets[i]._deleteGridImages(); dataSets[i].graphics._grid = {}; } iapi.updateVisual(); }, /* * Zooms an area when the scale factors are provided instead the rectangle dimensions. * Also useful when a pixelerated zooming is required e.g. in case of pinch zoom interactions. * @param zoomX {Number} - x- coordinate in pixels of the rectangle in original viewPortConfigurations * @param zoomY {Number} - y- coordinate in pixels of the rectangle in original viewPortConfigurations * @param scaleX {Number} - scale factors horizontally the zooming is required * @param scaleY {Number} - scale factors vertically the zooming is required * @param pixelatedDraw {Boolean} - Flag to zoom in pixelerated manner or redrawing is required. */ updateVisual: function (zoomX, zoomY, scaleX, scaleY, pixelatedDraw) { var xVisibleMin, xVisibleMax, yVisibleMin, yVisibleMax, i, clipRect, chart = this, chartConfig = chart.config, chartComponents = chart.components, viewPortConfig = chartConfig.viewPortConfig, oldCanvasWidth = chartConfig.canvasWidth, oldCanvasHeight = chartConfig.canvasHeight, newCanvasWidth = oldCanvasWidth, newCanvasHeight = oldCanvasHeight, viewPortHistory = chartConfig.viewPortHistory, lastViewPortConfig = viewPortHistory.slice(-1)[0], zoomEvent = [], viewPortStatus = chartConfig.status, initConfig = chartConfig._initConfig, xAxis = chartComponents.xAxis[0], yAxis = chartComponents.yAxis[0], chartGraphics = chart.graphics, quadrant = chartComponents.quadrant, quadrantContainer = quadrant && quadrant.graphics.container, maxZoomLimit = chartConfig.maxZoomLimit; //restrictions in the zooming limit. //check for validity for the input arguments provided. //Incase invalid, revert to the last viewPort configurations, that is visually there remains no change. viewPortConfig.x = isNaN(zoomX) ? (zoomX = lastViewPortConfig.x) : zoomX; viewPortConfig.y = isNaN(zoomY) ? (zoomY = lastViewPortConfig.y) : zoomY; viewPortConfig.scaleX = scaleX || (scaleX = lastViewPortConfig.scaleX); viewPortConfig.scaleY = scaleY || (scaleY = lastViewPortConfig.scaleY); // apply the limit if (scaleX > maxZoomLimit) { viewPortConfig.x = zoomX = mathMin(zoomX, (oldCanvasWidth - oldCanvasWidth/maxZoomLimit)); viewPortConfig.scaleX = scaleX = maxZoomLimit; } if (scaleY > maxZoomLimit) { viewPortConfig.y = zoomY = mathMin(zoomY, (oldCanvasHeight - oldCanvasHeight/maxZoomLimit)); viewPortConfig.scaleY = scaleY = maxZoomLimit; } if (pixelatedDraw) { //only applicable in case of pinch zoom interaction. chart.updateManager(pixelatedDraw); } else { //update the viewPortConfig.status to pan mode after every zoom, only if it is not an initial view. if (scaleX > 1 || scaleY > 1) { //incase the status is zoom after zooming in, then toogle it. if (viewPortStatus === 'zoom') { //updates the button visuals as well as the status is toogled. chart.toogleDragPan(true); } //it is a situation of zoomIn if (scaleX >= lastViewPortConfig.scaleX || scaleY >= lastViewPortConfig.scaleY) { //the new configuration is stored for future use. viewPortHistory.push({ scaleX: scaleX, scaleY: scaleY, x: zoomX, y: zoomY }); //stack of events to be raised. zoomEvent = ['zoomed','zoomIn']; } //situation of zoomOut. else { //pops the latest configuration from the viewPortHistory viewPortHistory.pop(); //overwrite the viewPortConfigurations with the latest stored configs and update them. lastViewPortConfig = viewPortHistory.slice(-1)[0]; viewPortConfig.x = lastViewPortConfig.x; viewPortConfig.y = lastViewPortConfig.y; viewPortConfig.scaleX = lastViewPortConfig.scaleX; viewPortConfig.scaleY = lastViewPortConfig.scaleY; //stack of events to be raised. zoomEvent = ['zoomed','zoomOut']; } } else { if (viewPortHistory.length > 1) { //atleast once the zooming has occured. viewPortHistory.pop(); } //In the initial view, it will be in zoom state, so in case in pan, toogling is required. if (viewPortStatus === 'pan') { chart.toogleDragPan(true); } //stack of events to be raised. zoomEvent = ['zoomed','zoomOut']; } //todo reduce the redaundancy. Updates the button visual only. chart.toogleDragPan(); newCanvasWidth = chartConfig.canvasWidth; newCanvasHeight = chartConfig.canvasHeight; // update the viewPortConfig x and y with the new canvas dimension if (zoomX) { zoomX = viewPortConfig.x = viewPortConfig.x * (newCanvasWidth / oldCanvasWidth); } if (zoomY) { viewPortConfig.y = viewPortConfig.y * (newCanvasHeight / oldCanvasHeight); } chartConfig.canvasLeft = initConfig.canvasLeft; chartConfig.canvasWidth = initConfig.canvasWidth; chartConfig.canvasHeight = initConfig.canvasHeight; chartConfig.canvasTop = initConfig.canvasTop; chartConfig.availableHeight = initConfig.availableHeight; chartConfig.availableWidth = initConfig.availableWidth; chart._spaceManager(); chart._postSpaceManagement(); /*On zoom draw all the datasets and the cosmetics needs to according to the cosmetic of that dataset when drawn initially*/ chart.updateManager(); //Redraw the axes on zooming xAxis.draw(); yAxis.draw(); chart._drawCanvas (); clipRect = { 'clip-rect' : chartConfig.canvasLeft + ',' + chartConfig.canvasTop + ',' + chartConfig.canvasWidth + ',' + chartConfig.canvasHeight }; if (chartConfig.viewPortConfig) { chart.graphics.imageContainer.attr({ x : chartConfig.canvasLeft, y : chartConfig.canvasRight }); } //update the image container wrt modified canvas dimension after resizing. chartGraphics.imageContainer.attr(clipRect) .transform('T' + chartConfig.canvasLeft + ',' + chartConfig.canvasTop); /*Clipping on the tracker is required when a single plot covers more than the screen and one hover on it, the hovering circle goes out of the canvas, which needs to be prevented and hence needs to be clipped. This clipping again needs to be updated with every resizing.*/ chartGraphics.tracker && chartGraphics.tracker.attr(clipRect); quadrantContainer && quadrantContainer.attr(clipRect); /* zero(0) and newCanvasWidth and newCanvasHeight denotes the extremes of the canvas area. So when getValue() is invoked with these inputs they fetches the minimum and maximum values of the x and y-axis in terms of axis value. */ xVisibleMin = xAxis.getValue(0); xVisibleMax = xAxis.getValue(newCanvasWidth); yVisibleMin = yAxis.getValue(0); yVisibleMax = yAxis.getValue(newCanvasHeight); //check if it is a first call ie. the unzoomed initial level. if (arguments.length === 0) { zoomEvent = BLANKSTRING; } for (i = 0; i < zoomEvent.length; i += 1) { //todo create global variables of 'zoomIn','zoomOut','reset' to avoid new instance of String class. if (zoomEvent[i] === 'zoomed') { /* * event to be raised when the plot is zoomed in or out. * level - the current level of viewPort configurations in the configuration stack. * startX - the visible minimum value of the x-axis * startY - the visible minimum value of the y-axis * endX - the visible maximum value of the x-axis * endY - the visible maximum value of the y-axis */ global.raiseEvent('zoomed', { level: viewPortHistory.length, startX: xVisibleMin, startY: yVisibleMin, endX: xVisibleMax, endY: yVisibleMax }, chart.chartInstance, [chart.chartInstance.id]); } if (zoomEvent[i] === 'zoomIn') { /* * event to be raised when the plot is zoomed in only. * level - the current level of viewPort configurations in the configuration stack. * startX - the visible minimum value of the x-axis * startY - the visible minimum value of the y-axis * endX - the visible maximum value of the x-axis * endY - the visible maximum value of the y-axis */ global.raiseEvent('zoomedIn', { level: viewPortHistory.length, startX: xVisibleMin, startY: yVisibleMin, endX: xVisibleMax, endY: yVisibleMax }, chart.chartInstance, [chart.chartInstance.id]); } else if (zoomEvent[i] === 'zoomOut' && !viewPortConfig.isReset) { /* * event to be raised when the plot is zoomed out only. * level - the current level of viewPort configurations in the configuration stack. * startX - the visible minimum value of the x-axis * startY - the visible minimum value of the y-axis * endX - the visible maximum value of the x-axis * endY - the visible maximum value of the y-axis */ global.raiseEvent('zoomedOut', { level: viewPortHistory.length, startX: xVisibleMin, startY: yVisibleMin, endX: xVisibleMax, endY: yVisibleMax }, chart.chartInstance, [chart.chartInstance.id]); } } } //hide the tracker element. chart.highlightPoint(false); //reset the isReset flag. viewPortConfig.isReset = false; }, //converts to a specified color corresponding to the given color and opacity. /* * Takes a color and opacity as input and converts the required rgba code. * @param color {String} - HEX color code * @param opacity {Number} - opacity in the range of 0-100 */ getFillColor: function (color, opacity) { //todo check if we have any similar library method opacity = parseFloat(opacity/100); // opacity is provided in [0-100] and needed to be in [0-1] if (opacity < 0) { // opacity cannot be negative. opacity = 0; } else if (opacity > 1) { // opacity cannot be more than 1 opacity = 1; } if (!color) { color = COLOR_WHITE; // white stays the default color } if (isIE && !hasSVG) {// if the version donot have SVG in IE. return opacity ? color : 'transparent'; } else { //convert to rgba code. color = color.replace(/^#?([a-f0-9]+)/ig, '$1'); color = lib.graphics.HEXtoRGB(color), color[3] = opacity.toString(); return 'rgba(' + color.join(',') + ')'; } }, /* function to find hovered trendlines */ _getHoveredTrendLine: function (point, trendLines) { var chart = this, chartComponents = chart.components, xAxis = chartComponents.xAxis[0], yAxis = chartComponents.yAxis[0], i, trendLinesLen = trendLines && trendLines.length, xPos = xAxis.getPixel(point.x), yPos = yAxis.getPixel(point.y), slope, trendObj, A, B, C, dist, x1, y1, x2, y2; if (!trendLinesLen) { return undefined; } for (i = trendLinesLen - 1; i >= 0; i--) { trendObj = trendLines[i]; x1 = trendObj.x1; y1 = trendObj.y1; x2 = trendObj.x2; y2 = trendObj.y2; if (trendObj.isTrendZone) { if (xPos >= x1 && xPos <= x2 && yPos >= y1 && yPos <= y2) { return trendObj; } } else { if (y1 !== y2 && x1 !== x2) { slope = (y1 - y2) / (x1 - x2); A = slope; B = -1; C = y1 - slope * x1; dist = mathAbs(A * xPos + B * yPos + C) / (mathSqrt(mathPow(A,2) + mathPow(B,2))); } else if (x1 === x2) { dist = mathAbs(x1 - xPos); } else if (y1 === y2) { dist = mathAbs(y1 - yPos); } if (dist <= trendObj.tolerance) { return trendObj; } } } }, /* * Search the nearest neighbouring point * Show the tooltip and the hover effects. * @param point {Object} - The position of the mouse move event wrt to the axis values. */ _bestNeighbour: function (point) { var chart = this, chartComponents = chart.components, chartConfig = chart.config, xAxis = chartComponents.xAxis[0], yAxis = chartComponents.yAxis[0], chartDef = chart.jsonData.chart, canvasLeft = chartConfig.canvasLeft, canvasTop = chartConfig.canvasTop, cx, cy, i, dist, showHoverEffect, pos, datasetObj, kdPoint, hoverPoint, hoverDistance, hoveredRadius, showToolTip, setTooltext, toolTipSepChar, seriesNameInToolTip, x, y, formatedVal, datasetConfig, label, toolText, numberFormatter = chartComponents.numberFormatter, datasets = chartComponents.dataset, lastIndex = datasets.length - 1; for (i = lastIndex; i >= 0; i -= 1) { datasetObj = datasets[i]; datasetConfig = datasetObj.config; showHoverEffect = datasetConfig.showHoverEffect; showToolTip = datasetConfig.showTooltip; if ((!datasetObj.components.kDTree) || !datasetObj.visible) { /*the search is not required either the tree is not build and mouse move occurs or that dataset is cuurently hidden after some legend interaction*/ continue; } //searches the nearest neighbouring point of the input point. kdPoint = datasetObj.getElement(point); //calculates the distance of the fetched neighbouring point and the recieved point. dist = chart._dist(point,kdPoint); if (!hoverPoint || dist < hoverDistance) { //if within a permissible distance the neighbouring point is found. pos = i; hoverPoint = kdPoint; //update the minimum of the distance among the other dataset. hoverDistance = dist; //zoomedRadius is the radius of the plot in a particular zoom level. hoveredRadius = ((datasets[pos] && datasetConfig.zoomedRadius) || 0); //if this distance is less than equal to radius break the loop if (hoverDistance <= hoveredRadius) { /*once a valid point(within the plot radius) is found within a dataset further serach among other dataset is not required.*/ break; } } } // if nearest point is within distance range highlight the tooltip if (hoverDistance <= mathMax((hoveredRadius + 2), 5)) { //convert to pixels for the center of the plot. /*cx = Math.round(startX + ((hoverPoint.x - minX) * xPVR)); cy = Math.round(startY + ((maxY - hoverPoint.y) * yPVR));*/ cx = xAxis.getPixel(hoverPoint.x) - canvasLeft; cy = yAxis.getPixel(hoverPoint.y) - canvasTop; setTooltext = pluck(hoverPoint.tooltext, datasetConfig.plotToolText); datasetObj = datasets[pos]; datasetConfig = datasetObj.config; toolTipSepChar = datasetConfig.tooltip.toolTipSepChar; seriesNameInToolTip = datasetConfig.tooltip.seriesNameInToolTip; x = hoverPoint.x; y = hoverPoint.y; if (hoverPoint) { //applying the axis specific number formatter required in tooltip formatedVal = numberFormatter.yAxis(y); label = numberFormatter.xAxis(x); if (Number(showToolTip)) { if (setTooltext !== undefined) { //support for macros in tooltip. toolText = parseTooltext(setTooltext, [4,5,6,7,8,9,10,11], { yaxisName: yAxis.getAxisConfig('axisName'), xaxisName: xAxis.getAxisConfig('axisName'), yDataValue: formatedVal, xDataValue: label }, hoverPoint, chartDef, datasetObj.config); } else { //determine the tooltext then. toolText = (seriesNameInToolTip ? (datasetConfig.seriesname ? (datasetConfig.seriesname + toolTipSepChar): '') : '') + 'x:' + label + toolTipSepChar + 'y:' + formatedVal; } } else { toolText = BLANKSTRING; } } //highlightPoint() is not called if the recent found point is same as the last hovered point. if (chartConfig.lastHoveredPoint !== hoverPoint) { //update the tracker circle and display the tooltext if required. chart.highlightPoint(showHoverEffect, cx, cy, hoverPoint, pos, toolText); } } else { // else hide the tooltip lib.toolTip.hide(); chart.highlightPoint(false); } return hoverPoint; }, /* function to draw tooltip of trendlines/zones or v-lines/zones */ _drawTooltip: function (originalEvent, tooltext) { var chart = this, chartComponents = chart.components, paper = chartComponents.paper, tip = lib.toolTip; if (tooltext) { tip.setStyle(paper); tip.setPosition(originalEvent); tip.draw(tooltext, paper); } }, /* * Highlight the hovered element. It needs the index value to set the cosmetics according to the dataset cosmetics * @param showHover {Boolean} - * @param cx {Number} - Pixel information about the x-cordinate of the center of the plot being hovered. * @param cy {Number} - Pixel information about the y-cordinate of the center of the plot being hovered. * @param point {Object} - Raw information about the point say its x and y. * @param index {Number} - This refers to the dataset index to which the point being hovered belongs to. * @param toolText {String} - Tooltext needed to be displayed for the point being hovered. */ highlightPoint: function (showHover,cx,cy,point,index, toolText) { var chart = this, chartConfig = chart.config, chartComponents = chart.components, chartGraphics = chart.graphics, paper = chartComponents.paper, tracker = chartGraphics.tracker, datasetObj = chartComponents.dataset[index], datasetConfig = datasetObj && datasetObj.config, radius = ( datasetObj && datasetConfig.zoomedRadius || 0), hoverCosmetics = datasetObj && datasetConfig.hoverCosmetics, fill = hoverCosmetics && hoverCosmetics.fill, borderColor = hoverCosmetics && hoverCosmetics.borderColor, borderThickness = hoverCosmetics && hoverCosmetics.borderThickness, attrObj = {}, //attach the callbacks for the click and hover interactions for the hovering element. plotEventHandlerCallback = { 'click': function (e){ lib.plotEventHandler.call(this, chart, e); }, 'hoverIn': function (e){ lib.plotEventHandler.call(this, chart, e, 'dataplotRollover'); }, 'hoverOut': function (e) { lib.plotEventHandler.call(this, chart, e, 'dataplotRollout'); } }, setLink = point && point.link; if (!tracker) { // in case the tracker element is not created. Attach the callbacks for click and hovering effects. tracker = chartGraphics.tracker = paper.circle(0, 0, 0, chartGraphics.trackerGroup) .attr({ 'clip-rect': chartConfig.canvasLeft + ',' + (chartConfig.canvasTop) + ',' + chartConfig.canvasWidth + ',' + chartConfig.canvasHeight }) .click(plotEventHandlerCallback.click) .trackTooltip(true) .hover(plotEventHandlerCallback.hoverIn, plotEventHandlerCallback.hoverOut); } //Attach the required information for the hovering element. point && tracker.data('eventArgs', { x: point.x, y: point.y, tooltip: toolText, link: setLink }); /* store the hovered point as last visible point. This is required to avoid redaundant calls if the same point is hovered. */ chartConfig.lastHoveredPoint = point; // if hover cosmetics then add that in the attrObj if (Number(showHover)) { attrObj = { r: radius, fill: fill, stroke: borderColor, 'stroke-width': borderThickness }; } else { // make transparent configuration attrObj = { r: radius, fill: TRACKER_FILL, stroke: TRACKER_FILL, 'stroke-width': 0 }; } attrObj.cursor = setLink ? POINTER : '', tracker .attr(attrObj) .tooltip(toolText) .transform('t' + (cx + chartConfig.canvasLeft) + ',' + (cy + chartConfig.canvasTop)); /*on first mouse move the element is created and on the next mouse move the tooltip is shown. In order give the effect of displaying the tooltip once hovered, another mouseMove event is fired forcefully */ point && fireMouseEvent('mouseover', tracker && tracker.node, chartConfig.lastMouseEvent); }, zoom: function (level) { var newX, newY, newEndX, newEndY, chart = this, chartConfig = chart.config, viewPortConfig = chartConfig.viewPortConfig, oldScaleX = viewPortConfig.scaleX, oldScaleY = viewPortConfig.scaleY, oldX = viewPortConfig.x, oldY = viewPortConfig.y, canvasW = chartConfig.canvasWidth, canvasH = chartConfig.canvasHeight, scale1w = canvasW / oldScaleX, scale1h = canvasH / oldScaleY, newscale1w = scale1w *level, newscale1h = scale1h *level; // should not cross canvas limit newscale1w = newscale1w > canvasW ? canvasW : newscale1w; newscale1h = newscale1h > canvasH ? canvasH : newscale1h; // calculate the new x and y newX = (oldX + (scale1w / 2)) - (newscale1w / 2); newY = (oldY + (scale1h / 2)) - (newscale1h / 2); // new x and y should not be less than 0 newX = newX < 0 ? 0 : newX; newY = newY < 0 ? 0 : newY; // new visible area should not cross the original size newEndX = newX + newscale1w; newEndY = newY + newscale1h; newX = newEndX > canvasW ? (canvasW - newscale1w) : newX; newY = newEndY > canvasH ? (canvasH - newscale1h) : newY; chart.zoomSelection(newX, newY, newscale1w, newscale1h); }, /*converts the buttons visual into activated/ deactivated modes. e.g. deactivation of reset button iniitially. buttonArr stores the array list of the buttons, whose visuals needs to be modified. tooltip of the buttons needs to be modified inaccordance to its activated / deactivated states.*/ updateButtonVisual: function () { var i, button, buttonDisableArr, buttonEnableArr, chart = this, chartConfig = chart.config, chartComponents = chart.components, toolBoxGraphics = chartComponents.tb.graphics, viewPortHistory = chartConfig.viewPortHistory, status = chartConfig.status, pressedButton; // atleast once zoomed. if (viewPortHistory.length <= 1) { buttonDisableArr = ['zoomOutButton', 'resetButton', 'togglePanButton']; buttonEnableArr = ['zoomInButton']; pressedButton = 'toggleZoomInButton'; } // intial unzoomed view else { buttonEnableArr = ['resetButton', 'zoomOutButton', 'zoomInButton']; buttonDisableArr = []; if (status === 'zoom') { buttonEnableArr.push('togglePanButton'); pressedButton = 'toggleZoomInButton'; } else if (status === 'pan'){ buttonEnableArr.push('toggleZoomInButton'); pressedButton = 'togglePanButton'; } } for (i = 0; i < buttonDisableArr.length; i += 1) { button = buttonDisableArr[i]; //convert the button visual to deactivated state with blank tooltip. toolBoxGraphics[button].node.attr({ config: { hover: { fill: COLOR_WHITE, 'stroke-width': 1, stroke: COLOR_E3E3E3, cursor: STR_DEF }, normal: { fill: COLOR_WHITE, stroke: COLOR_E3E3E3, 'stroke-width': 1, cursor: STR_DEF }, disable: { fill: COLOR_WHITE, 'stroke-width': 1, stroke: COLOR_E3E3E3, 'stroke-opacity': 1, cursor: STR_DEF }, pressed: { fill: COLOR_WHITE, 'stroke-width': 1, stroke: COLOR_E3E3E3, cursor: STR_DEF } }, fill: [COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, true], 'button-disabled': false, 'stroke': COLOR_E3E3E3, 'stroke-opacity': 1 }); } for (i = 0; i < buttonEnableArr.length; i += 1) { button = buttonEnableArr[i]; //convert the button visial to activated state with appropiate tooltip. toolBoxGraphics[button].node.attr({ config: { hover: { fill: COLOR_WHITE, 'stroke-width': 1, stroke: '#aaaaaa', cursor: 'pointer' }, normal: { fill: COLOR_WHITE, stroke: COLOR_C2C2C2, 'stroke-width': 1, cursor: 'pointer' }, disable: { fill: COLOR_WHITE, 'stroke-width': 1, stroke: COLOR_E3E3E3, 'stroke-opacity': 1, cursor: 'pointer' }, pressed: { fill: COLOR_EFEFEF, 'stroke-width': 1, stroke: COLOR_C2C2C2, cursor: 'pointer' } }, 'button-disabled': false, fill: [COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, true], 'stroke': COLOR_C2C2C2, 'stroke-opacity': 1 }); } toolBoxGraphics[pressedButton].node.attr({ config: { hover: { fill: COLOR_EFEFEF, 'stroke-width': 1, stroke: COLOR_EFEFEF, cursor: STR_DEF }, normal: { fill: COLOR_EFEFEF, stroke: COLOR_E3E3E3, 'stroke-width': 1, cursor: STR_DEF }, disable: { fill: COLOR_E6E6E6, 'stroke-width': 1, stroke: COLOR_E3E3E3, 'stroke-opacity': 1, cursor: STR_DEF }, pressed: { fill: COLOR_EFEFEF, 'stroke-width': 1, stroke: COLOR_E3E3E3, cursor: STR_DEF } }, 'button-disabled': false, fill: [COLOR_EFEFEF, COLOR_EFEFEF, COLOR_EFEFEF, COLOR_EFEFEF, true], 'stroke': COLOR_E3E3E3, 'stroke-opacity': 1 }); }, _drawAxis: function () { } }, chartAPI.scatterBase); FusionCharts.register('component', ['dataset', 'zoomscatter', { type: 'zoomscatter', configure: function () { var plotFillHoverColor, plotFillHoverAlpha, borderHoverColor, borderHoverThickness, borderHoverAlpha, userGivenBorderColor, conf, showHoverEffect, dataSetComponents, staticRadius, dataSet = this, chart = dataSet.chart, chartConfig = chart.config, chartAttr = chart.jsonData.chart, getFillColor = chart.getFillColor, JSONData = dataSet.JSONData; // configurations parsed as those of the scatter chart. dataSet.__base__.configure.call(dataSet); //store a reference to the configuration parsed. conf = dataSet.config; dataSetComponents = dataSet.components; //Following configurations are added or modified being speciifc to the 'zoom-scatter' chart // We first look into dataset then chart obj and then default value.(plucking the cosmetics) userGivenBorderColor = pluck(JSONData.anchorbordercolor, chartAttr.anchorbordercolor); /*If no border Thickness is given, the border thickness turns to 1 px in case there exists a user-defined anchorBorderColor; orelse the anchorBorderThickness turns to zero.*/ conf.anchorBorderThickness = pluck(JSONData.anchorborderthickness, chartAttr.anchorborderthickness, userGivenBorderColor ? 1 : 0); dataSetComponents.kDTree = new KdTree(); // create a new instance for the kdTree class. // * @todo make this dynamic as per the browser performance conf.chunkSize = Math.floor(mathMin(((JSONData.data || []).length)/5,50000)); // Turning staticRadius TRUE, keeps the radius of the plots intact even after zooming. staticRadius = conf.staticRadius = pluckNumber(chartAttr.staticradius, 0); // Applies to all the plots of a particular dataset. conf.radius = pluckNumber(JSONData.radius,JSONData.anchorradius,chartAttr.radius, chartAttr.anchorradius, (staticRadius ? 3 : 0.5)); showHoverEffect = conf.showHoverEffect; //Hover Cosmetics plotFillHoverColor = getFirstColor(pluck(JSONData.plotfillhovercolor, JSONData.hovercolor, chartAttr.plotfillhovercolor, chartAttr.hovercolor, conf.anchorbgcolor)), plotFillHoverAlpha = pluck(JSONData.plotfillhoveralpha, JSONData.hoveralpha, chartAttr.plotfillhoveralpha, chartAttr.hoveralpha, HUNDREDSTRING), borderHoverColor = getFirstColor(pluck(JSONData.plotfillhovercolor, JSONData.hovercolor, chartAttr.plotfillhovercolor, chartAttr.hovercolor, plotFillHoverColor)), borderHoverAlpha = pluck(JSONData.plotfillhoveralpha, JSONData.hoveralpha, chartAttr.plotfillhoveralpha, chartAttr.hoveralpha, HUNDREDSTRING), borderHoverThickness = pluckNumber(JSONData.borderhoverthickness, chartAttr.borderhoverthickness, 1), conf.hoverCosmetics = { 'showHoverEffect': showHoverEffect, 'fill': getFillColor(plotFillHoverColor,plotFillHoverAlpha), 'borderColor': getFillColor(borderHoverColor,borderHoverAlpha), 'borderThickness': borderHoverThickness }; //store the hoverCosmetics //sending an examplory // conf.hoverCosmetics = dataSet._parseHoverEffectOptions(); //tooltip configurations conf.tooltip = { toolTipVisible: chartConfig.showtooltip, seriesNameInToolTip: chartConfig.seriesnameintooltip, toolTipSepChar: chartConfig.tooltipsepchar }; // create the store to store last scale factors conf.lastViewPort = {}; }, /* * check for zoom or pan operations and start the _gridDraw() * @param pixelatedDraw {Boolean} - During the pinchZoom interactions in touch enabled devices, this flag is set to true to have a pixelerated drawing mode to let the user know to what extent they are zooming into */ draw: function (pixelatedDraw) { var xAxis, yAxis, xPVR, yPVR, zoomedRadius, datasetObj = this, dataSetGraphics = datasetObj.graphics, chart = datasetObj.chart, chartComponents = chart.components, chartGraphics = chart.graphics, imageContainer = chartGraphics.imageContainer, paper = chartComponents.paper, chartConfig = chart.config, imageGroup = dataSetGraphics.container, visible = datasetObj.visible, viewPortConfig = chartConfig.viewPortConfig, datasetConfig = datasetObj.config, // Refernece to the configurations particular to the dataset instance lastViewPort = datasetConfig.lastViewPort || {}, // store the latest viewPort Configurations quickInitialDraw = false; /*a flag to check if there is a modification in the lastViewPort and determine if the action is supposedly zoom or paning one.*/ //create the image group if not being created if (!imageGroup) { // This grid-container is the container element for all the images required for that dataset itself. imageGroup = (dataSetGraphics.container = paper.group('grid-container',imageContainer)); } /* Invoke the hide or show function, only if ambigious cases arises like the _conatinerHidden is set to true whilst the dataSet is in visible state. */ if (visible && datasetObj._conatinerHidden) { datasetObj.show(); } else if (!visible && !datasetObj._conatinerHidden) { datasetObj.hide(); } if (pixelatedDraw) { datasetObj._pixelatedDraw(); /* Draws in the pixelerated mode giving a better visual clue during pinch zoom interaction*/ } else { //modifications in viewPortConfig indicates it is a zoom effect and not pan. if (lastViewPort.scaleX !== viewPortConfig.scaleX || lastViewPort.scaleY !== viewPortConfig.scaleY) { //This is a zoom action as the viewPortConfigurations are modified. // update the lastViewPort Configurations lastViewPort.scaleX = viewPortConfig.scaleX; lastViewPort.scaleY = viewPortConfig.scaleY; //sets the x and y limit for search implementation. xAxis = chartComponents.xAxis[0]; yAxis = chartComponents.yAxis[0]; // Get the latest PixelValueRatio from the axis modules. xPVR = xAxis.getPVR(); yPVR = yAxis.getPVR(); /*For zoomedRadius below 2 pixels, it becomes tough for tooltip display, hence minimum 2 pixels is the lower cut-off.Similarily, zoomedDiameter can not be expected to be more than the canvasWidth or canvasHeight becuase there is no point of letting the user zoom beyond a level where only a single plot covers the entire canvas area*/ zoomedRadius = mathMax((datasetConfig.zoomedRadius = mathMin((datasetConfig.staticRadius ? datasetConfig.radius : (datasetConfig.radius * mathMin(viewPortConfig.scaleX, viewPortConfig.scaleY))), chartConfig.canvasWidth / 2, chartConfig.canvasHeight / 2)), 2); // reset the configurations which are dependent on zoom level datasetObj.components.kDTree._setSearchLimit(zoomedRadius/xPVR, zoomedRadius/yPVR); datasetObj._deleteGridImages(); // delete the old grids. datasetObj.graphics._grid = {}; //initialise the _grid Object. quickInitialDraw = true; // in case of zoom do not create a seperate thread for the first drawing } // now draw the grid image datasetObj._gridDraw(quickInitialDraw); } }, /* * Pixelerates the entire canvas drawing. This is useful in pinchZoom interaction where the redraw part is not required untill the interaction is completed. * Clear the previous drawing threads. * Pixelerate(Modify the posiition and dimentions of) the images in accordance to the modified viewPort configurations. */ _pixelatedDraw: function () { var rowIndex, colIndex, imageElem, gridElem, lineImage, row, datasetObj = this, chart = datasetObj.chart, chartConfig = chart.config, datasetConfig = datasetObj.config, dataSetGraphics = datasetObj.graphics, drawLine = datasetConfig.drawLine, lastViewPort = datasetConfig.lastViewPort || {}, oldSX = lastViewPort.scaleX, oldSY = lastViewPort.scaleY, viewPortConfig = chartConfig.viewPortConfig, newSX = viewPortConfig.scaleX, newSY = viewPortConfig.scaleY, grid = datasetObj.graphics._grid || (datasetObj.graphics._grid = []), scaleFactorX = newSX / oldSX, // Factor by which the scaleX have been changed in the new configurations scaleFactorY = newSY / oldSY, // Factor by which the scaleY have been changed in the new configurations gridWidth = chartConfig.canvasWidth * scaleFactorX, gridHeight = chartConfig.canvasHeight * scaleFactorY, batchDarwTimers = datasetConfig._batchDarwTimers, imageGroup = dataSetGraphics.container; // clear previous drading thread if any clearTimeout(datasetObj.timer); // delete previous drawing threads for grids if (batchDarwTimers && batchDarwTimers.length) { while (batchDarwTimers.length) { clearTimeout(batchDarwTimers.shift()); } } // pan the image group imageGroup.transform('t'+mathRound(-viewPortConfig.x * viewPortConfig.scaleX)+',' + mathRound(-viewPortConfig.y * viewPortConfig.scaleY)); // zoom all the images. grid is the 2-d array consisting of the image elements. for (rowIndex in grid) { row = grid[rowIndex]; if (row) { for (colIndex in row) { gridElem = row[colIndex]; if (gridElem && gridElem.drawState) { imageElem = gridElem.image; //update the image elements positions and dimentions. imageElem.attr({ 'x' : gridElem.xPixel * scaleFactorX, 'y' : gridElem.yPixel * scaleFactorY, 'width': gridWidth, 'height': gridHeight }); if (drawLine) { lineImage = gridElem.lineImage; //update the image elements positions and dimentions. lineImage.attr({ 'x' : gridElem.xPixel * scaleFactorX, 'y' : gridElem.yPixel * scaleFactorY, 'width': gridWidth, 'height': gridHeight }); } } } } } }, /* * Delete the already drawn images * Delete previous drawing threads */ _deleteGridImages: function () { var imageElem, lineImage, lineCanvas, canvasElem, gridElem, rowIndex, colIndex, row, datasetObj = this, datasetConfig = datasetObj.config, datasetGraphics = datasetObj.graphics, imagePool =datasetGraphics._imagePool || (datasetGraphics._imagePool = []), canvasPool = datasetGraphics._canvasPool || (datasetGraphics._canvasPool = []), lineImagePool =datasetGraphics._lineImagePool || (datasetGraphics._lineImagePool = []), lineCanvasPool = datasetGraphics._lineCanvasPool || (datasetGraphics._lineCanvasPool = []), grid = datasetGraphics._grid || [], batchDarwTimers = datasetConfig._batchDarwTimers; // delete previous drawing threads if (batchDarwTimers && batchDarwTimers.length) { while (batchDarwTimers.length) { clearTimeout(batchDarwTimers.shift()); } } for ( rowIndex in grid) { row = grid[rowIndex]; if (row) { for (colIndex in row) { gridElem = row[colIndex]; if (gridElem && gridElem.drawState) { // unlink the image element imageElem = gridElem.image; //blanks the src of the image element. imageElem.attr({ 'src': '', 'width': 0, 'height': 0 }); imagePool.push(imageElem); //push the already drawn image in the image pool delete gridElem.image; // unlink the canvas element canvasElem = gridElem.canvas; canvasPool.push(canvasElem); //push the already drawn canvas in the canvas pool delete gridElem.canvas; delete gridElem.ctx; if ((lineImage = gridElem.lineImage)) { //blanks the src of the line image element. lineImage.attr({ 'src': '', 'width': 0, 'height': 0 }); lineImagePool.push(lineImage); //push the already drawn image in the image pool delete gridElem.lineImage; // unlink the canvas element lineCanvas = gridElem.lineCanvas; lineCanvasPool.push(lineCanvas); //push the already drawn canvas in the canvas pool delete gridElem.lineCanvas; delete gridElem.lineCtx; } } } } } // delete the grid store delete datasetGraphics._grid; }, /* * Primarily updates the gridManager using _gridManager(). But in case of pan, it does in timer to avoid mouse freezing. So even if the drawing part becomes heavy, they dont block mouse drag event, enriching the UI experience, * @param quickInitialDraw {Boolean} - The flag is TRUE for zooming action and FALSE during the panning actions. */ _gridDraw: function (quickInitialDraw) { var datasetObj = this, dataSetGraphics = datasetObj.graphics, datasetConfig = datasetObj.config, chart = datasetObj.chart, chartConfig = chart.config, viewPortConfig = chartConfig.viewPortConfig; // clear previous drading thread if any clearTimeout(datasetConfig.timer); /*Pan the image group to the latest viewPortConfigurations taking the scaling factors(due to zooming) in account*/ dataSetGraphics.container.transform('t'+mathRound(-viewPortConfig.x * viewPortConfig.scaleX)+',' + mathRound(-viewPortConfig.y * viewPortConfig.scaleY)); if (quickInitialDraw) { //Zoom actions // draw grid members if required datasetObj._gridManager(); } else { //Pan(/Drag) actions. //_gridManager() is called in setTimeout() to avoid frezzed mousemove as the drawing is heavy datasetConfig.timer = setTimeout (function () { // draw grid members if required datasetObj._gridManager(); }, 10); } }, /* * initially draw max 4 grid (grid of the 4 sample corner point) which are visible and then draw the rest images */ _gridManager: function () { var rowGrid, element, xMinValue, xMaxValue, yMinValue, yMaxValue, gridIndex, centerRowNo, centerColNo, returnObj, startRow, startCol, endRow, endCol, i, j, rowObj, callBack, datasetObj = this, datasetGraphics = datasetObj.graphics, datasetConfig = datasetObj.config, grid = datasetGraphics._grid, drawGrid = datasetConfig._drawGrid || (datasetConfig._drawGrid = []), chart = datasetObj.chart, chartConfig = chart.config, chartComponents = chart.components, viewPortConfig = chartConfig.viewPortConfig, scaleX = viewPortConfig.scaleX, scaleY = viewPortConfig.scaleY, noRow = Math.ceil(scaleY), noCol = Math.ceil(scaleX), xAxis = chartComponents.xAxis[0], yAxis = chartComponents.yAxis[0], cellWidth = xAxis.getAxisConfig('axisDimention').axisLength || chartConfig.canvasWidth, cellHeight = chartConfig.canvasHeight, xAxisConfig = xAxis.config, yAxisConfig = yAxis.config, xMin = xAxisConfig.axisRange.min, xMax = xAxisConfig.axisRange.max, yMin = yAxisConfig.axisRange.min, yMax = yAxisConfig.axisRange.max, cellXlength = (xMax - xMin) / scaleX, cellYlength = (yMax - yMin) / scaleY, radius = datasetConfig.radius * mathMin(viewPortConfig.scaleX,viewPortConfig.scaleY), borderWidth = datasetConfig.plotCosmetics.borderWidth, padPx = radius + borderWidth, xRadiusPad = mathAbs(padPx / (cellWidth * viewPortConfig.scaleX/(xAxis.max - xAxis.min))), yRadiusPad = mathAbs(padPx / (cellHeight * viewPortConfig.scaleY/(yAxis.max - yAxis.min))), viewPortOrigW = cellWidth / scaleX, viewPortOrigH = cellHeight / scaleY, // distance of the sample 4 corner point from the view-port border gridPadding = mathMin(1, viewPortOrigW / 10, viewPortOrigH / 10), p1GridStore = {}, // grids that will be drawn in phase-1 // grids that will be added later. //Use Array as this will be directly passed to `_drawGridArr` p2GridStore = [], // get the 4 corner pixel leftX = viewPortConfig.x + gridPadding, rightX = viewPortConfig.x - gridPadding + viewPortOrigW, topY = viewPortConfig.y + gridPadding, bottomY = viewPortConfig.y + viewPortOrigH - gridPadding; // initially draw max 4 grid (grid of the 4 sample corner point) which are visisble // grid of the top-left corner gridIndex = datasetObj._getFocusedGrid(leftX, topY); rowObj = p1GridStore[gridIndex.row] || (p1GridStore[gridIndex.row] = {}); rowObj[gridIndex.col] = true; // grid of the top-right corner gridIndex = datasetObj._getFocusedGrid(rightX, topY); rowObj = p1GridStore[gridIndex.row] || (p1GridStore[gridIndex.row] = {}); rowObj[gridIndex.col] = true; // grid of the bottom-right corner gridIndex = datasetObj._getFocusedGrid(rightX, bottomY); rowObj = p1GridStore[gridIndex.row] || (p1GridStore[gridIndex.row] = {}); rowObj[gridIndex.col] = true; // grid of the bottom-left corner gridIndex = datasetObj._getFocusedGrid(leftX, bottomY); rowObj = p1GridStore[gridIndex.row] || (p1GridStore[gridIndex.row] = {}); rowObj[gridIndex.col] = true; /* @todo: support animantion*/ // get the center grid returnObj = datasetObj._getFocusedGrid(); centerRowNo = returnObj.row;// the center row index centerColNo = returnObj.col; // the center column index //configurations required to traverse across the center image. startRow = mathMax(centerRowNo - 1, 0); endRow = mathMin(centerRowNo + 1, noRow - 1); startCol = mathMax(centerColNo - 1, 0); endCol = mathMin(centerColNo + 1, noCol - 1); yMaxValue = yMax - (startRow * cellYlength); /*keeping the center grid, get the neighbouring images if present and create the drawGrid to store the grids to be drawn*/ for (i = startRow; i <= endRow; i += 1) { rowGrid = grid[i] || (grid[i] = {}); xMinValue = xMin + (startCol * cellXlength); for (j = startCol; j <= endCol; j+=1) { element = grid[i][j]; if (!element) { yMinValue = mathMax(yMaxValue - cellYlength, yMin); xMaxValue = mathMin(xMinValue + cellXlength, xMax); //update the grid element configurations. element = grid[i][j] = { xPixel: j * cellWidth, yPixel: i * cellHeight, xMinValue: xMinValue, yMinValue: yMinValue, xMaxValue: xMaxValue, yMaxValue: yMaxValue, drawState: 0, // 0=> not drawn, 1=> drawn, 2=> is drawing // padding to accomodate the partial drawing of the elements of neighbouring grid xMinWPad: mathMax(xMinValue - xRadiusPad, xMin), yMinWPad: mathMax(yMinValue - yRadiusPad, yMin), xMaxWPad: mathMin(xMaxValue + xRadiusPad, xMax), yMaxWPad: mathMin(yMaxValue + yRadiusPad, yMax) }; } if (!element.drawState) { if (p1GridStore[i] && p1GridStore[i][j]) { //build the stack of the images needed to be drawn in the first phase. drawGrid.push({ row : i, col : j }); } else { p2GridStore.push({ //build the stack of the images needed to be drawn in the next phase. row : i, col : j }); } } xMinValue += cellXlength; //increment the xMinValue after every image is drawn horizontally. } yMaxValue -= cellYlength; //decrement the yMaxValue after every image is drawn vertically. } //The visible images are drawn first and the rest images are drawn as a callback of the rest. if (drawGrid.length || p2GridStore.length) { // create the call back function that will be called once these grids are created callBack = function () { datasetConfig._drawGrid = p2GridStore; // No callback after the 2nd phase datasetObj._drawGridArr(); }; // draw the images for 1st phase datasetObj._drawGridArr(callBack); } }, /* * Returns the center grid element row and column wrt to a given x,y or based on canvas center. * @param x {Number} - x-Coordinate wrt which the grid element center is to be found. * @param y {NUmber} - y-coordinate wrt which the grid element center is to be tracked. * @return {Object} - The row and column for the center grid element. */ _getFocusedGrid: function (x, y) { // get the focused grid based on the grid center and not the left top. var datasetObj = this, chart = datasetObj.chart, chartConfig = chart.config, viewPortConfig = chartConfig.viewPortConfig, scaleX = viewPortConfig.scaleX, scaleY = viewPortConfig.scaleY, canvasWidth = chartConfig.canvasWidth, canvasHeight = chartConfig.canvasHeight, //the width and height wrt original unzoomed initial level. canOrigW = canvasWidth / scaleX, canOrigH = canvasHeight / scaleY, //In case the x,y is not defined, the canvas center in its current zoom level is choosen. xPixel = isNaN(x) ? viewPortConfig.x + (canOrigW / 2) : x, yPixel = isNaN(y) ? viewPortConfig.y + (canOrigH / 2) : y; return { 'row': mathFloor(yPixel / canOrigH), 'col': mathFloor(xPixel / canOrigW) }; }, /* * * @param callBack {Function} - Once the four visible images are drawn, this function sets the _drawGrid to the rest images, which are drawn again. Hence this callback is invoked at the end of the completion of drawing of the first phase images. */ _drawGridArr: function (callBack) { // draw grids and delete them from the drawGridarr once completed var gridIndexElem, gridElem, canvasElem, lineCanvasElem, ctx, lineCtx, datasetObj = this, chart = datasetObj.chart, chartConfig = chart.config, chartComponents = chart.components, viewPortConfig = chartConfig.viewPortConfig, datasetConfig = datasetObj.config, drawLine = datasetConfig.drawLine, gridIndexArr = datasetConfig._drawGrid, // list of images to be drawn. gridSubArr = [], gridContainer = datasetObj.graphics.container, paper = chartComponents.paper, width = chartComponents.xAxis[0].getAxisConfig('axisDimention').axisLength || chartConfig.canvasWidth, height = chartConfig.canvasHeight, grid = datasetObj.graphics._grid, imagePool = datasetObj.graphics._imagePool || [], canvasPool = datasetObj.graphics._canvasPool || [], lineImagePool = datasetObj.graphics._lineImagePool || [], lineCanvasPool = datasetObj.graphics._lineCanvasPool || [], plotCosmetics = datasetConfig.plotCosmetics, //update the radius with every zoom configurations. radius = datasetConfig.radius * mathMin(viewPortConfig.scaleX, viewPortConfig.scaleY), offset = (chart.components.xAxis[0].getAxisConfig('axisDimention').x || chartConfig.canvasLeft) - chartConfig.canvasLeft; if (gridIndexArr.length) { while (gridIndexArr.length) { gridIndexElem = gridIndexArr.shift(); gridElem = grid[gridIndexElem.row][gridIndexElem.col]; if (gridElem.drawState === 2) { continue; } gridElem.drawState = 2; // use another set of images for drawing the lines. if (drawLine) { // add the image from the pool if (lineImagePool.length) { gridElem.lineImage = lineImagePool.shift(); } else { // create the image element. gridElem.lineImage = paper.image('', gridContainer); } // set the image dimensions. gridElem.lineImage.attr({ 'x' : gridElem.xPixel + offset, 'y' : gridElem.yPixel, 'width' : (width), 'height' : (height) }); // add the canvas element from the pool if (canvasPool.length) { gridElem.lineCanvas = lineCanvasElem = lineCanvasPool.shift(); } else { // create the canvas if it doesnot exist. gridElem.lineCanvas = lineCanvasElem = win.document.createElement('canvas'); } // set the canvas dimensions lineCanvasElem.setAttribute('width', width); lineCanvasElem.setAttribute('height', height); // cache the context of the canvas element. lineCtx = gridElem.lineCtx = lineCanvasElem.getContext('2d'); // apply the cosmetics of the anchors. lineCtx.fillStyle = plotCosmetics.fillStyle; lineCtx.strokeStyle = plotCosmetics.lineStrokeStyle; lineCtx.lineWidth = plotCosmetics.lineWidth; } // add the image from the pool if (imagePool.length) { gridElem.image = imagePool.shift(); } else { // create the image element. gridElem.image = paper.image('', gridContainer); } // set the image dimensions. gridElem.image.attr({ 'x' : gridElem.xPixel + offset, 'y' : gridElem.yPixel, 'width' : (width), 'height' : (height) }); // add the canvas element from the pool if (canvasPool.length) { gridElem.canvas = canvasElem = canvasPool.shift(); } else { // create the canvas if it doesnot exist. gridElem.canvas = canvasElem = win.document.createElement('canvas'); } // set the canvas dimensions canvasElem.setAttribute('width', width); canvasElem.setAttribute('height', height); ctx = gridElem.ctx = canvasElem.getContext('2d'); // cache the context of the canvas element. // apply the cosmetics of the anchors. if (radius < 1) { /* incase of very small radius, set the fill as stroke-style and draw a dot. This is acts as a leverage on performance.*/ ctx.strokeStyle= plotCosmetics.fillStyle; ctx.lineWidth = 0.5; } else { ctx.fillStyle = plotCosmetics.fillStyle; ctx.strokeStyle = plotCosmetics.strokeStyle; ctx.lineWidth = plotCosmetics.borderWidth; } gridSubArr.push(gridElem); } // reset the batch drawing index datasetConfig._batchDrawindex = (datasetObj.JSONData.data && datasetObj.JSONData.data.length - 1) || 0; //start drawing the images in batches. datasetObj._drawGridArrBatch(gridSubArr, callBack, !datasetConfig.animation.enabled); } else { // if there is nothing to draw then call the callBack callBack && callBack(); } }, /* * Draw the grids of the array together in batch. * At the batch end call the call back. * Use grid's own image and canvas only. * At the end set the drawState of the grids. * Draw in batches so that it naver goes for script time out. * @param gridArr {Array} - Stores the grid Elements, which has its own indivual images, canvas elements and contexts * @param callBack {Function} - Function being called at the end of all drawings of the images in the gridArr. * @param doNotUpdateImage {Boolean} - Flag to update the visual configurations. If set to true, At every batch the images are viusally updated. Once set to false they are shown only if all the drawing is completed. This is attained using showAnimation = 0 */ _drawGridArrBatch : function (gridArr, callBack, doNotUpdateImage) { var cx, cy, cx1, cx2, cy1, cy2, y1, y2, storeX, j, gridElem, ctx, lineCtx, element, minX, xMinValue, yMaxValue, leftElement, lineImage, lineCanvas, minY, maxX, maxY, image, canvas, regresionPoints, datasetObj = this, conf = datasetObj.config, chart = datasetObj.chart, chartConfig = chart.config, chartComponents = chart.components, width = chartConfig.canvasWidth, datasetConfig = datasetObj.config, drawLine = datasetConfig.drawLine, plotCosmetics = datasetConfig.plotCosmetics, i = datasetConfig._batchDrawindex, arr = datasetObj.components.data, chunkSize = datasetConfig.chunkSize, endIndex = i - chunkSize, xAxis = chartComponents.xAxis[0], yAxis = chartComponents.yAxis[0], radius = datasetConfig.zoomedRadius, xPVR = xAxis.getPVR(), yPVR = yAxis.getPVR(), regressionStatus = conf.showRegressionLine, showYonX, regressionLineColor, regressionLineThickness, //stores the already plotted pixels for caching and performance improvement. _store = datasetConfig._store || [], // doStroke for the plot is set to TRUE only if there exists a lineWidth or radius is less than 1 pixel doStroke = (plotCosmetics.lineWidth || (radius < 1)); if (regressionStatus) { showYonX = conf.showYOnX; regressionLineColor = conf.regLineColor; regressionLineThickness = conf.regressionLineThickness; } //clear all the previous visual for the canvas grid and update its cosmetics. for (j = 0; j < gridArr.length; j += 1) { gridArr[j].ctx.beginPath(); if (drawLine) { gridArr[j].lineCtx.beginPath(); } } endIndex = endIndex <= 0 ? 0 : endIndex; // lower limit is 0 for (; i >= endIndex; i-=1) { element = arr[i] && arr[i].config.setValue; //Check for NaN value. if (!element || isNaN(element.x) || isNaN(element.y)) { continue; } //Check which grid is the element lying and draw it in that grid. for (j = 0; j < gridArr.length; j += 1) { gridElem = gridArr[j]; xMinValue = gridElem.xMinValue; yMaxValue = gridElem.yMaxValue; if ((element.x < gridElem.xMinWPad || element.x > gridElem.xMaxWPad) || (element.y < gridElem.yMinWPad || element.y > gridElem.yMaxWPad)) { continue; } ctx = gridElem.ctx; lineCtx = gridElem.lineCtx; cx = mathRound((element.x - xMinValue) * xPVR); // value to pixel conversions cy = mathRound((yMaxValue - element.y) * yPVR); // value to pixel conversions /* Incase there is already a point being drawn with exact same center pixelwise, there is no need to draw again. This can happen when two data points are very closely placed. This caching is done in _store */ storeX = _store[cx]; if (!storeX) { storeX = _store[cx] = {}; } if (!storeX[cy]) { storeX[cy] = true; //set the flag of a circle being drawn at that pixel to TRUE if (drawLine) { leftElement = ((i - 1) >= 0) && arr[i - 1].config.setValue; if (leftElement) { lineCtx.moveTo(mathRound((leftElement.x - xMinValue) * xPVR), mathRound((yMaxValue - leftElement.y) * yPVR)); lineCtx.lineTo(cx, cy); } } if (radius < 1){ /* Drawing a dot seemed to have a performance preference to drawing an arc. So if radius turns less than 1 pixel, drawing a dot is prefered. */ ctx.moveTo(cx, cy); ctx.lineTo(cx+1, cy); } else { ctx.moveTo(cx+radius, cy); ctx.arc(cx,cy,radius,0,pi2); } } } } //clear all the previous visual for the canvas grid and update its cosmetics. for (j = 0; j < gridArr.length; j += 1) { gridElem = gridArr[j]; ctx = gridElem.ctx; ctx.fill(); doStroke && ctx.stroke(); ctx.closePath(); if (drawLine) { lineCtx = gridElem.lineCtx; lineCtx.fill(); doStroke && lineCtx.stroke(); lineCtx.closePath(); } } // reset the _batchDrawindex datasetConfig._batchDrawindex = i; // if there is any remaning drawing to be drawn if (i >= 0) { if (!doNotUpdateImage) { //update all the grid images for the visual clue. for (j = 0; j < gridArr.length; j += 1){ image = gridArr[j].image; canvas = gridArr[j].canvas; // update the src of the images. image.attr({ 'src': canvas.toDataURL('image/png') }); if (datasetConfig.drawLine) { lineImage = gridArr[j].lineImage; lineCanvas = gridArr[j].lineCanvas; // update the src of the images. lineImage.attr({ 'src': lineCanvas.toDataURL('image/png') }); } } } // store the timers to future cancellation (datasetConfig._batchDarwTimers || (datasetConfig._batchDarwTimers = [])).push( setTimeout(function () { datasetObj.chart && datasetObj._drawGridArrBatch(gridArr, callBack, doNotUpdateImage); }, 0)); } // drawing completed else { // remove the temp store arr delete datasetConfig._store; //regresion line generalised for all images. if (regressionStatus) { regresionPoints = conf.regressionData; // temp code need to remove /*if already the regression analysis is complete, update the drawing part by scaling correctly instead re-calculating*/ /*if (!regresionPoints) { regresionPoints = ; datasetObj.components.regressionObj.regresionPoints = regresionPoints; }*/ /*0 pixel in the canvas is the origin and no offset */ //update all the grid images for the regressionLine. for (j = 0; j < gridArr.length; j += 1) { gridElem = gridArr[j]; minX= gridElem.xMinValue; minY = gridElem.yMinValue; maxX = gridElem.xMaxValue; maxY = gridElem.yMaxValue; image = gridElem.image; canvas = gridElem.canvas; ctx = gridElem.ctx; // extend the points to make the regressionLine throughout the canvas. cx1 = ((regresionPoints[0].x - minX) * xPVR); cy1 = ((maxY - regresionPoints[0].y) * yPVR); cx2 = ((regresionPoints[1].x - minX) * xPVR); cy2 = ((maxY - regresionPoints[1].y) * yPVR); y1 = (((cy2 - cy1) * (0 - cx2))/(cx2 - cx1)) + cy2; y2 = (((cy2 - cy1) * (width - cx2))/(cx2 - cx1)) + cy2; ctx.beginPath(); ctx.strokeStyle = regressionLineColor; ctx.lineWidth = regressionLineThickness; ctx.moveTo(cx1, cy1); ctx.lineTo(cx2, cy2); ctx.stroke(); ctx.closePath(); } } //update all the grid images for the visual clue. for (j = 0; j < gridArr.length; j += 1) { gridElem = gridArr[j]; image = gridElem.image; canvas = gridElem.canvas; gridElem.drawState = 1; // set the drawState flag as drawn. image.attr({ 'src': canvas.toDataURL('image/png') }); if (drawLine) { lineImage = gridElem.lineImage; lineCanvas = gridElem.lineCanvas; lineImage.attr({ 'src': lineCanvas.toDataURL('image/png') }); } } if (!datasetObj.tree) { // create the kdtree in a seperate thread setTimeout(function (){ datasetObj._buildKdTree(); },250); } // invoke the completion callBack callBack && callBack(); } }, /* * Builds the tree structure and store them inside the dataset instance. * A temporary cloning of the original data may be required to avoid permannent manipulations in the original array */ _buildKdTree: function () { var datasetObj = this, datasetConfig = datasetObj.config, JSONData = datasetObj.JSONData; /* Create a replica of the original JSON data to avoid manipulations in the original array. This replica is created and destroyed only for the sole purpose of making the tree. Once the tree is made, its deleted and is no more required.*/ datasetConfig._kdPoints = (JSONData.data || []).slice(); if (!datasetObj.components.kDTree) { //Create a new instance of the KdTree class and store its reference in the current instance. datasetObj.components.kDTree = new KdTree(); } datasetObj.components.kDTree.buildKdTree(datasetConfig._kdPoints); // Build the tree structure. delete datasetConfig._kdPoints; // delete the replicated array. }, /* * Its mainly an abstraction that fetches the neighbouring point using the kdTree class instance. * @param point {Object} - The x and y- coordinate whose neighbouring point is to be hunted. * @return {Object} - The neighbouring point of the input point fetched within that partiuclar dataset. */ getElement: function (point) { var datasetObj = this; if (datasetObj.components.kDTree) { // searches the neighbouring points using the kdtree instance. return datasetObj.components.kDTree.getNeighbour(point); } }, show: function () { var dataSet = this, dataSetGraphics = dataSet.graphics, container = dataSetGraphics && dataSetGraphics.container; dataSet.visible = true; dataSet._conatinerHidden = false; container.show(); }, hide: function () { var dataSet = this, dataSetGraphics = dataSet.graphics, container = dataSetGraphics && dataSetGraphics.container; dataSet.visible = false; dataSet._conatinerHidden = true; container.hide(); }, _addLegend: function () { var dataSet = this, chart = dataSet.chart, chartAttr = chart.jsonData.chart, conf = dataSet.config, JSONData = dataSet.JSONData, getFillColor = chart.getFillColor, userGivenBorderColor = pluck(JSONData.anchorbordercolor, chartAttr.anchorbordercolor), seriesAnchorBorderColor = getFirstColor(pluck(userGivenBorderColor, conf.plotBorderColor)), seriesAnchorBorderThickness = pluckNumber(JSONData.anchorborderthickness, chartAttr.anchorborderthickness, userGivenBorderColor ? 1 : 0), seriesAnchorBgColor = getFirstColor(pluck(JSONData.anchorbgcolor, JSONData.color, chartAttr.anchorbgcolor,conf.plotColor)), seriesAnchorAlpha = pluck(JSONData.anchoralpha, JSONData.alpha, chartAttr.anchoralpha, HUNDREDSTRING), seriesAnchorBgAlpha = pluck(JSONData.anchorbgalpha, JSONData.alpha, chartAttr.anchorbgalpha, HUNDREDSTRING), lineColorObj = { color: conf.lineColor, alpha: conf.lineAlpha }, //save the cosmetics finally plucked for re-drawing them in future plotCosmetics = conf.plotCosmetics = { 'fillStyle': getFillColor(seriesAnchorBgColor,((seriesAnchorAlpha * seriesAnchorBgAlpha) / 100)), 'strokeStyle': getFillColor(seriesAnchorBorderColor,seriesAnchorAlpha), 'borderWidth': seriesAnchorBorderThickness, lineWidth: conf.lineThickness, 'lineStrokeStyle': toRaphaelColor(lineColorObj) }; dataSet.legendItemId = chart.components.legend.addItems(dataSet, dataSet.legendInteractivity, { enabled: conf.includeInLegend, type : dataSet.type, fillColor : plotCosmetics.fillStyle, strokeColor: plotCosmetics.strokeStyle, anchorSide: 2, strokeWidth: conf.anchorBorderThickness, label : getFirstValue(dataSet.JSONData.seriesname) }); }, /* * Sets the configurations for the set level attributes. */ _setConfigure: function () { var i, config, dataObj, setData, setValue, toolText, toolTipValue, macroIndices, parserConfig, formatedVal, formatedValX, setDisplayValue, infMin = -Infinity, infMax = +Infinity, yMax = infMin, yMin = infMax, xMin = infMax, xMax = infMin, dataSet = this, dataStore = dataSet.components.data || (dataSet.components.data = []), chart = dataSet.chart, chartComponents = chart.components, parseUnsafeString = lib.parseUnsafeString, conf = dataSet.config, JSONData = dataSet.JSONData, //chart level JSON format chartAttr = chart.jsonData.chart, setDataArr = JSONData.data || [], dataSetLen = setDataArr.length, numberFormatter = chartComponents.numberFormatter, yAxisName = parseUnsafeString(chartAttr.yaxisname), xAxisName = parseUnsafeString(chartAttr.xaxisname), lineDashed = conf.lineDashed, lineDashStyle = conf.lineDashStyle, parentYAxis = conf.parentYAxis, tooltipSepChar = conf.toolTipSepChar, seriesname = conf.seriesname; // Iterate through all set level data for (i = 0; i < dataSetLen; i += 1) { setData = setDataArr[i]; dataObj = dataStore[i] || (dataStore[i] = {}); config = dataObj.config || (dataObj.config = {}); //The set data is given: {x: , y : } config.setValue = setValue = { x: numberFormatter.getCleanValue(setData.x), y: numberFormatter.getCleanValue(setData.y), index: i }; xMax = mathMax(xMax, setValue.x); xMin = mathMin(xMin, setValue.x); yMax = mathMax(yMax, setValue.y); yMin = mathMin(yMin, setValue.y); //update the regression calulations. conf.showRegressionLine && this.pointValueWatcher(setValue.x, setValue.y, conf.regressionObj); config.setLink = pluck(setData.link); // Parsing the anchor properties for set level config.anchorProps = this._parseAnchorProperties(i); config.showValue = pluckNumber(setData.showvalue, conf.showValues); // Dashed, color and alpha configuration in set level is only for line chart config.dashed = pluckNumber(setData.dashed, lineDashed); config.color = pluck(setData.color, conf.lineColor); config.alpha = pluck(setData.alpha, conf.lineAlpha); config.dashStyle = config.dashed ? lineDashStyle : 'none'; config.toolTipValue = toolTipValue = numberFormatter.dataLabels(setValue.y, parentYAxis); config.setDisplayValue = setDisplayValue = parseUnsafeString(setData.displayvalue); formatedVal = (config.formatedVal = pluck(setData.toolTipValue, numberFormatter.dataLabels(setValue.y, parentYAxis))); formatedValX = numberFormatter.xAxis(setValue.x); config.displayValue = pluck(setDisplayValue, toolTipValue); config.setTooltext = lib.getValidValue(parseUnsafeString(pluck(setData.tooltext, conf.plotToolText))); // Initial tooltext parsing if (!conf.showTooltip) { toolText = false; } else if (config.setTooltext !== undefined) { macroIndices = [4,5,6,7,8,9,10,11]; parserConfig = { yaxisName: yAxisName, xaxisName: xAxisName, yDataValue: formatedVal, xDataValue: formatedValX }; toolText = parseTooltext(config.setTooltext, macroIndices, parserConfig, setData, chartAttr, JSONData); } //determine the default tooltext then. else { if (formatedVal === null) { toolText = false; } else { toolText = seriesname ? seriesname + tooltipSepChar : BLANKSTRING; toolText += setValue.x ? formatedValX + tooltipSepChar : BLANKSTRING; toolText += toolTipValue; } } config.toolText = toolText; if (!dataObj) { dataObj = dataStore[i] = { graphics : {} }; } else if (!dataObj.graphics) { dataStore[i].graphics = {}; } //parse the hover cosmetics. config.hoverEffects = this._parseHoverEffectOptions(dataObj); config.anchorProps.isAnchorHoverRadius = config.hoverEffects.anchorRadius; } conf.xMax = xMax; conf.xMin = xMin; conf.yMin = yMin; conf.yMax = yMax; //augment the regression line to be a line series. if (conf.showRegressionLine) { //get a line series. conf.regressionData = this.getRegressionLineSeries(conf.regressionObj, conf.showYOnX, dataSetLen); } dataSet.ErrorValueConfigure && dataSet.ErrorValueConfigure(); } }, 'Scatter']); //Adding the custom tool symbol R.addSymbol({ zoomInIcon: function (x, y, radius) { var icoX = x - radius * 0.2, icoY = y - radius * 0.2, rad = radius * 0.8, startAngle = R.rad(43), // to prevent cos and sin of start and end from becoming equal on 360 arcs endAngle = R.rad(48), startX = icoX + rad * mathCos(startAngle), startY = icoY + rad * mathSin(startAngle), endX = icoX + rad * mathCos(endAngle), endY = icoY + rad * mathSin(endAngle), handleHeight = radius, // the height of the handle handAngle = R.rad(45), handX1 = startX + handleHeight * mathCos(handAngle), handY1 = startY + handleHeight * mathSin(handAngle), handX2 = endX + handleHeight * mathCos(handAngle) - 1, handY2 = endY + handleHeight * mathSin(handAngle) - 1, semiW = 2; return ['M', startX , startY, 'A', rad, rad, 0, 1, 0, endX, endY, 'Z', 'M', startX + 1 , startY + 1 , 'L', handX1, handY1, handX2, handY2, endX + 1, endY + 1, 'Z', 'M', icoX - semiW, icoY, 'L', icoX + semiW, icoY, 'Z','M', icoX, icoY - semiW, 'L', icoX , icoY+semiW, 'Z']; }, zoomModeIcon: function (cx,cy,radius){ var x = cx, path = [], y = cy, icoX = x - radius * 0.2, icoY = y - radius * 0.2, rad = radius * 0.8, startAngle = R.rad(43), // to prevent cos and sin of start and end from becoming equal on 360 arcs endAngle = R.rad(48), startX = icoX + rad * mathCos(startAngle), startY = icoY + rad * mathSin(startAngle), endX = icoX + rad * mathCos(endAngle), endY = icoY + rad * mathSin(endAngle), handleHeight = radius, // the height of the handle handAngle = R.rad(45), handX1 = startX + handleHeight * mathCos(handAngle), handY1 = startY + handleHeight * mathSin(handAngle), handX2 = endX + handleHeight * mathCos(handAngle) - 1, handY2 = endY + handleHeight * mathSin(handAngle) - 1, semiW = 2; path = path.concat(['M', startX , startY, 'A', rad, rad, 0, 1, 0, endX, endY, 'Z', 'M', startX + 1 , startY + 1 , 'L', handX1, handY1, handX2, handY2, handX2+1, handY2-1, handX2+1.5, handY2+1.5, handX2-1, handY2+1,handX2, handY2, endX + 1, endY + 1, 'Z', 'M', icoX - semiW, icoY, 'L', icoX + semiW, icoY, 'Z','M', icoX, icoY - semiW, 'L', icoX , icoY+semiW, 'Z']); return path; }, panModeIcon: function (cx,cy,radius){ var x = cx - (45/4), y = cy; //draw the pan(move) icon. x = cx; radius *= 2.5; return [].concat(['M', (x - (radius/16)) , (y - (radius/8)),'L',(x + (radius/16)) , (y - (radius/8)),'L', (x + (radius/16)) , (y - (radius/3.2)), 'L',(x + (6*radius/32)) , (y - (10*radius/32)),'L', x , (y - radius/2),'L',(x - (6*radius/32)) , (y - (10*radius/32)), 'L',(x - (radius/16)) , (y - (radius/3.2)),'Z', 'M', (x + (4*radius/32)) , (y-(2*radius/32)),'L',(x + (10*radius/32)) , (y-(2*radius/32)),'L', (x + (10*radius/32)) , (y-(6*radius/32)), 'L',(x + (16*radius/32)) , y,'L', (x+(10*radius/32)) , (y+(6*radius/32)),'L',(x + (10*radius/32)) , (y+(2*radius/32)),'L', (x + (4*radius/32)) , (y+(2*radius/32)),'Z', 'M', (x + (2*radius/32)) , (y + (5*radius/32)),'L',(x + (2*radius/32)) , (y+(10*radius/32)),'L', (x + (6*radius/32)) , (y+(10*radius/32)), 'L',x , (y+(16*radius/32)),'L', (x-(6*radius/32)) , (y+(10*radius/32)),'L',(x - (2*radius/32)) , (y+(10*radius/32)),'L', (x - (2*radius/32)) ,(y+(5*radius/32)),'Z', 'M', (x - (4*radius/32)) , (y - (2*radius/32)),'L',(x - (10*radius/32)) , (y - (2*radius/32)), 'L', (x - (10*radius/32)) , (y-(6*radius/32)), 'L',(x-(16*radius/32)) , y,'L', (x-(10*radius/32)) , (y+(6*radius/32)),'L',(x - (10*radius/32)) , (y+(2*radius/32)),'L', (x - (4*radius/32)) , (y+(2*radius/32)),'Z']); } }); }]); }));