mirror of
https://github.com/bendtherules/line_chart_demo.git
synced 2026-08-18 13:52:30 +00:00
4155 lines
190 KiB
JavaScript
4155 lines
190 KiB
JavaScript
|
|
(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 <http://www.fusioncharts.com/license>
|
|
*
|
|
* @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 <http://www.fusioncharts.com/license>
|
|
*
|
|
* @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, '<strong>Zoom in</strong><br/>Or double-' +
|
|
'click on plot to zoom-in');
|
|
conf.btnSelectZoomToolText = pluck(chartAttr.btnselectzoomtooltext,
|
|
'<strong>Select a region to zoom-in</strong><br/>Click to enable pan mode.');
|
|
conf.btnPanToolText = pluck(chartAttr.btnpantooltext,
|
|
'<strong>Drag to move across chart</strong><br/>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: <Number>, y : <Number>}
|
|
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']);
|
|
}
|
|
});
|
|
}]);
|
|
|
|
|
|
}));
|