ag-charts-community
Version:
Advanced Charting / Charts supporting Javascript / Typescript / React / Angular / Vue
592 lines • 21.6 kB
JavaScript
"use strict";
var __read = (this && this.__read) || function (o, n) {
var m = typeof Symbol === "function" && o[Symbol.iterator];
if (!m) return o;
var i = m.call(o), r, ar = [], e;
try {
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
}
catch (error) { e = { error: error }; }
finally {
try {
if (r && !r.done && (m = i["return"])) m.call(i);
}
finally { if (e) throw e.error; }
}
return ar;
};
Object.defineProperty(exports, "__esModule", { value: true });
var matrix_1 = require("./matrix");
var id_1 = require("../util/id");
var PointerEvents;
(function (PointerEvents) {
PointerEvents[PointerEvents["All"] = 0] = "All";
PointerEvents[PointerEvents["None"] = 1] = "None";
})(PointerEvents = exports.PointerEvents || (exports.PointerEvents = {}));
/**
* Abstract scene graph node.
* Each node can have zero or one parent and belong to zero or one scene.
*/
var Node = /** @class */ (function () {
function Node() {
/**
* Unique node ID in the form `ClassName-NaturalNumber`.
*/
this.id = id_1.createId(this);
/**
* Some number to identify this node, typically within a `Group` node.
* Usually this will be some enum value used as a selector.
*/
this.tag = NaN;
/**
* To simplify the type system (especially in Selections) we don't have the `Parent` node
* (one that has children). Instead, we mimic HTML DOM, where any node can have children.
* But we still need to distinguish regular leaf nodes from container leafs somehow.
*/
this.isContainerNode = false;
this._children = [];
// Used to check for duplicate nodes.
this.childSet = {}; // new Set<Node>()
// These matrices may need to have package level visibility
// for performance optimization purposes.
this.matrix = new matrix_1.Matrix();
this.inverseMatrix = new matrix_1.Matrix();
this._dirtyTransform = false;
this._scalingX = 1;
this._scalingY = 1;
/**
* The center of scaling.
* The default value of `null` means the scaling center will be
* determined automatically, as the center of the bounding box
* of a node.
*/
this._scalingCenterX = null;
this._scalingCenterY = null;
this._rotationCenterX = null;
this._rotationCenterY = null;
/**
* Rotation angle in radians.
* The value is set as is. No normalization to the [-180, 180) or [0, 360)
* interval is performed.
*/
this._rotation = 0;
this._translationX = 0;
this._translationY = 0;
/**
* Each time a property of the node that effects how it renders changes
* the `dirty` property of the node should be set to `true`. The change
* to the `dirty` property of the node will propagate up to its parents
* and eventually to the scene, at which point an animation frame callback
* will be scheduled to rerender the scene and its nodes and reset the `dirty`
* flags of all nodes and the {@link Scene._dirty | Scene} back to `false`.
* Since changes to node properties are not rendered immediately, it's possible
* to change as many properties on as many nodes as needed and the rendering
* will still only happen once in the next animation frame callback.
* The animation frame callback is only scheduled if it hasn't been already.
*/
this._dirty = true;
this._visible = true;
this.dirtyZIndex = false;
this._zIndex = 0;
this.pointerEvents = PointerEvents.All;
}
/**
* This is meaningfully faster than `instanceof` and should be the preferred way
* of checking inside loops.
* @param node
*/
Node.isNode = function (node) {
return node ? node.matrix !== undefined : false;
};
Node.prototype._setScene = function (value) {
this._scene = value;
var children = this.children;
var n = children.length;
for (var i = 0; i < n; i++) {
children[i]._setScene(value);
}
};
Object.defineProperty(Node.prototype, "scene", {
get: function () {
return this._scene;
},
enumerable: true,
configurable: true
});
Node.prototype._setParent = function (value) {
this._parent = value;
};
Object.defineProperty(Node.prototype, "parent", {
get: function () {
return this._parent;
},
enumerable: true,
configurable: true
});
Object.defineProperty(Node.prototype, "children", {
get: function () {
return this._children;
},
enumerable: true,
configurable: true
});
Node.prototype.countChildren = function (depth) {
if (depth === void 0) { depth = Node.MAX_SAFE_INTEGER; }
if (depth <= 0) {
return 0;
}
var children = this.children;
var n = children.length;
var size = n;
for (var i = 0; i < n; i++) {
size += children[i].countChildren(depth - 1);
}
return size;
};
/**
* Appends one or more new node instances to this parent.
* If one needs to:
* - move a child to the end of the list of children
* - move a child from one parent to another (including parents in other scenes)
* one should use the {@link insertBefore} method instead.
* @param nodes A node or nodes to append.
*/
Node.prototype.append = function (nodes) {
// Passing a single parameter to an open-ended version of `append`
// would be 30-35% slower than this.
if (Node.isNode(nodes)) {
nodes = [nodes];
}
// The function takes an array rather than having open-ended
// arguments like `...nodes: Node[]` because the latter is
// transpiled to a function where the `arguments` object
// is copied to a temporary array inside a loop.
// So an array is created either way. And if we already have
// an array of nodes we want to add, we have to use the prohibitively
// expensive spread operator to pass it to the function,
// and, on top of that, the copy of the `arguments` is still made.
var n = nodes.length;
for (var i = 0; i < n; i++) {
var node = nodes[i];
if (node.parent) {
throw new Error(node + " already belongs to another parent: " + node.parent + ".");
}
if (node.scene) {
throw new Error(node + " already belongs a scene: " + node.scene + ".");
}
if (this.childSet[node.id]) {
// Cast to `any` to avoid `Property 'name' does not exist on type 'Function'`.
throw new Error("Duplicate " + node.constructor.name + " node: " + node);
}
this._children.push(node);
this.childSet[node.id] = true;
node._setParent(this);
node._setScene(this.scene);
}
this.dirty = true;
};
Node.prototype.appendChild = function (node) {
if (node.parent) {
throw new Error(node + " already belongs to another parent: " + node.parent + ".");
}
if (node.scene) {
throw new Error(node + " already belongs to a scene: " + node.scene + ".");
}
if (this.childSet[node.id]) {
// Cast to `any` to avoid `Property 'name' does not exist on type 'Function'`.
throw new Error("Duplicate " + node.constructor.name + " node: " + node);
}
this._children.push(node);
this.childSet[node.id] = true;
node._setParent(this);
node._setScene(this.scene);
this.dirty = true;
return node;
};
Node.prototype.removeChild = function (node) {
if (node.parent === this) {
var i = this.children.indexOf(node);
if (i >= 0) {
this._children.splice(i, 1);
delete this.childSet[node.id];
node._setParent();
node._setScene();
this.dirty = true;
return node;
}
}
throw new Error("The node to be removed is not a child of this node.");
};
/**
* Inserts the node `node` before the existing child node `nextNode`.
* If `nextNode` is null, insert `node` at the end of the list of children.
* If the `node` belongs to another parent, it is first removed.
* Returns the `node`.
* @param node
* @param nextNode
*/
Node.prototype.insertBefore = function (node, nextNode) {
var parent = node.parent;
if (node.parent) {
node.parent.removeChild(node);
}
if (nextNode && nextNode.parent === this) {
var i = this.children.indexOf(nextNode);
if (i >= 0) {
this._children.splice(i, 0, node);
this.childSet[node.id] = true;
node._setParent(this);
node._setScene(this.scene);
}
else {
throw new Error(nextNode + " has " + parent + " as the parent, "
+ "but is not in its list of children.");
}
this.dirty = true;
}
else {
this.append(node);
}
return node;
};
Object.defineProperty(Node.prototype, "nextSibling", {
get: function () {
var parent = this.parent;
if (parent) {
var children = parent.children;
var index = children.indexOf(this);
if (index >= 0 && index <= children.length - 1) {
return children[index + 1];
}
}
},
enumerable: true,
configurable: true
});
Node.prototype.transformPoint = function (x, y) {
var matrix = matrix_1.Matrix.flyweight(this.matrix);
var parent = this.parent;
while (parent) {
matrix.preMultiplySelf(parent.matrix);
parent = parent.parent;
}
return matrix.invertSelf().transformPoint(x, y);
};
Node.prototype.inverseTransformPoint = function (x, y) {
var matrix = matrix_1.Matrix.flyweight(this.matrix);
var parent = this.parent;
while (parent) {
matrix.preMultiplySelf(parent.matrix);
parent = parent.parent;
}
return matrix.transformPoint(x, y);
};
Object.defineProperty(Node.prototype, "dirtyTransform", {
get: function () {
return this._dirtyTransform;
},
set: function (value) {
this._dirtyTransform = value;
if (value) {
this.dirty = true;
}
},
enumerable: true,
configurable: true
});
Object.defineProperty(Node.prototype, "scalingX", {
get: function () {
return this._scalingX;
},
set: function (value) {
if (this._scalingX !== value) {
this._scalingX = value;
this.dirtyTransform = true;
}
},
enumerable: true,
configurable: true
});
Object.defineProperty(Node.prototype, "scalingY", {
get: function () {
return this._scalingY;
},
set: function (value) {
if (this._scalingY !== value) {
this._scalingY = value;
this.dirtyTransform = true;
}
},
enumerable: true,
configurable: true
});
Object.defineProperty(Node.prototype, "scalingCenterX", {
get: function () {
return this._scalingCenterX;
},
set: function (value) {
if (this._scalingCenterX !== value) {
this._scalingCenterX = value;
this.dirtyTransform = true;
}
},
enumerable: true,
configurable: true
});
Object.defineProperty(Node.prototype, "scalingCenterY", {
get: function () {
return this._scalingCenterY;
},
set: function (value) {
if (this._scalingCenterY !== value) {
this._scalingCenterY = value;
this.dirtyTransform = true;
}
},
enumerable: true,
configurable: true
});
Object.defineProperty(Node.prototype, "rotationCenterX", {
get: function () {
return this._rotationCenterX;
},
set: function (value) {
if (this._rotationCenterX !== value) {
this._rotationCenterX = value;
this.dirtyTransform = true;
}
},
enumerable: true,
configurable: true
});
Object.defineProperty(Node.prototype, "rotationCenterY", {
get: function () {
return this._rotationCenterY;
},
set: function (value) {
if (this._rotationCenterY !== value) {
this._rotationCenterY = value;
this.dirtyTransform = true;
}
},
enumerable: true,
configurable: true
});
Object.defineProperty(Node.prototype, "rotation", {
get: function () {
return this._rotation;
},
set: function (value) {
if (this._rotation !== value) {
this._rotation = value;
this.dirtyTransform = true;
}
},
enumerable: true,
configurable: true
});
Object.defineProperty(Node.prototype, "rotationDeg", {
get: function () {
return this.rotation / Math.PI * 180;
},
/**
* For performance reasons the rotation angle's internal representation
* is in radians. Therefore, don't expect to get the same number you set.
* Even with integer angles about a quarter of them from 0 to 359 cannot
* be converted to radians and back without precision loss.
* For example:
*
* node.rotationDeg = 11;
* console.log(node.rotationDeg); // 10.999999999999998
*
* @param value Rotation angle in degrees.
*/
set: function (value) {
this.rotation = value / 180 * Math.PI;
},
enumerable: true,
configurable: true
});
Object.defineProperty(Node.prototype, "translationX", {
get: function () {
return this._translationX;
},
set: function (value) {
if (this._translationX !== value) {
this._translationX = value;
this.dirtyTransform = true;
}
},
enumerable: true,
configurable: true
});
Object.defineProperty(Node.prototype, "translationY", {
get: function () {
return this._translationY;
},
set: function (value) {
if (this._translationY !== value) {
this._translationY = value;
this.dirtyTransform = true;
}
},
enumerable: true,
configurable: true
});
Node.prototype.containsPoint = function (x, y) {
return false;
};
/**
* Hit testing method.
* Recursively checks if the given point is inside this node or any of its children.
* Returns the first matching node or `undefined`.
* Nodes that render later (show on top) are hit tested first.
* @param x
* @param y
*/
Node.prototype.pickNode = function (x, y) {
if (!this.visible || this.pointerEvents === PointerEvents.None || !this.containsPoint(x, y)) {
return;
}
var children = this.children;
if (children.length) {
// Nodes added later should be hit-tested first,
// as they are rendered on top of the previously added nodes.
for (var i = children.length - 1; i >= 0; i--) {
var hit = children[i].pickNode(x, y);
if (hit) {
return hit;
}
}
}
else if (!this.isContainerNode) { // a leaf node, but not a container leaf
return this;
}
};
Node.prototype.computeBBox = function () { return; };
Node.prototype.computeBBoxCenter = function () {
var bbox = this.computeBBox && this.computeBBox();
if (bbox) {
return [
bbox.x + bbox.width * 0.5,
bbox.y + bbox.height * 0.5
];
}
return [0, 0];
};
Node.prototype.computeTransformMatrix = function () {
// TODO: transforms without center of scaling and rotation correspond directly
// to `setAttribute('transform', 'translate(tx, ty) rotate(rDeg) scale(sx, sy)')`
// in SVG. Our use cases will mostly require positioning elements (rects, circles)
// within a group, rotating groups at right angles (e.g. for axis) and translating
// groups. We shouldn't even need `scale(1, -1)` (invert vertically), since this
// can be done using D3-like scales already by inverting the output range.
// So for now, just assume that centers of scaling and rotation are at the origin.
// const [bbcx, bbcy] = this.computeBBoxCenter();
var _a = __read([0, 0], 2), bbcx = _a[0], bbcy = _a[1];
var sx = this.scalingX;
var sy = this.scalingY;
var scx;
var scy;
if (sx === 1 && sy === 1) {
scx = 0;
scy = 0;
}
else {
scx = this.scalingCenterX === null ? bbcx : this.scalingCenterX;
scy = this.scalingCenterY === null ? bbcy : this.scalingCenterY;
}
var r = this.rotation;
var cos = Math.cos(r);
var sin = Math.sin(r);
var rcx;
var rcy;
if (r === 0) {
rcx = 0;
rcy = 0;
}
else {
rcx = this.rotationCenterX === null ? bbcx : this.rotationCenterX;
rcy = this.rotationCenterY === null ? bbcy : this.rotationCenterY;
}
var tx = this.translationX;
var ty = this.translationY;
// The transform matrix `M` is a result of the following transformations:
// 1) translate the center of scaling to the origin
// 2) scale
// 3) translate back
// 4) translate the center of rotation to the origin
// 5) rotate
// 6) translate back
// 7) translate
// (7) (6) (5) (4) (3) (2) (1)
// | 1 0 tx | | 1 0 rcx | | cos -sin 0 | | 1 0 -rcx | | 1 0 scx | | sx 0 0 | | 1 0 -scx |
// M = | 0 1 ty | * | 0 1 rcy | * | sin cos 0 | * | 0 1 -rcy | * | 0 1 scy | * | 0 sy 0 | * | 0 1 -scy |
// | 0 0 1 | | 0 0 1 | | 0 0 1 | | 0 0 1 | | 0 0 1 | | 0 0 0 | | 0 0 1 |
// Translation after steps 1-4 above:
var tx4 = scx * (1 - sx) - rcx;
var ty4 = scy * (1 - sy) - rcy;
this.dirtyTransform = false;
this.matrix.setElements([
cos * sx, sin * sx,
-sin * sy, cos * sy,
cos * tx4 - sin * ty4 + rcx + tx,
sin * tx4 + cos * ty4 + rcy + ty
]).inverseTo(this.inverseMatrix);
};
Object.defineProperty(Node.prototype, "dirty", {
get: function () {
return this._dirty;
},
set: function (value) {
// TODO: check if we are already dirty (e.g. if (this._dirty !== value))
// if we are, then all parents and the scene have been
// notified already, and we are doing redundant work
// (but test if this is indeed the case)
this._dirty = value;
if (value) {
if (this.parent) {
this.parent.dirty = true;
}
else if (this.scene) {
this.scene.dirty = true;
}
}
},
enumerable: true,
configurable: true
});
Object.defineProperty(Node.prototype, "visible", {
get: function () {
return this._visible;
},
set: function (value) {
if (this._visible !== value) {
this._visible = value;
this.dirty = true;
}
},
enumerable: true,
configurable: true
});
Object.defineProperty(Node.prototype, "zIndex", {
get: function () {
return this._zIndex;
},
set: function (value) {
if (this._zIndex !== value) {
this._zIndex = value;
if (this.parent) {
this.parent.dirtyZIndex = true;
}
this.dirty = true;
}
},
enumerable: true,
configurable: true
});
Node.MAX_SAFE_INTEGER = Math.pow(2, 53) - 1; // Number.MAX_SAFE_INTEGER
return Node;
}());
exports.Node = Node;
//# sourceMappingURL=node.js.map