ag-charts-community
Version:
Advanced Charting / Charts supporting Javascript / Typescript / React / Angular / Vue
502 lines • 17.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const matrix_1 = require("./matrix");
const 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.
*/
class Node {
constructor() {
/**
* 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
*/
static isNode(node) {
return node ? node.matrix !== undefined : false;
}
_setScene(value) {
this._scene = value;
const children = this.children;
const n = children.length;
for (let i = 0; i < n; i++) {
children[i]._setScene(value);
}
}
get scene() {
return this._scene;
}
_setParent(value) {
this._parent = value;
}
get parent() {
return this._parent;
}
get children() {
return this._children;
}
countChildren(depth = Node.MAX_SAFE_INTEGER) {
if (depth <= 0) {
return 0;
}
const children = this.children;
const n = children.length;
let size = n;
for (let 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.
*/
append(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.
const n = nodes.length;
for (let i = 0; i < n; i++) {
const 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;
}
appendChild(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;
}
removeChild(node) {
if (node.parent === this) {
const 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
*/
insertBefore(node, nextNode) {
const parent = node.parent;
if (node.parent) {
node.parent.removeChild(node);
}
if (nextNode && nextNode.parent === this) {
const 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;
}
get nextSibling() {
const { parent } = this;
if (parent) {
const { children } = parent;
const index = children.indexOf(this);
if (index >= 0 && index <= children.length - 1) {
return children[index + 1];
}
}
}
transformPoint(x, y) {
const matrix = matrix_1.Matrix.flyweight(this.matrix);
let parent = this.parent;
while (parent) {
matrix.preMultiplySelf(parent.matrix);
parent = parent.parent;
}
return matrix.invertSelf().transformPoint(x, y);
}
inverseTransformPoint(x, y) {
const matrix = matrix_1.Matrix.flyweight(this.matrix);
let parent = this.parent;
while (parent) {
matrix.preMultiplySelf(parent.matrix);
parent = parent.parent;
}
return matrix.transformPoint(x, y);
}
set dirtyTransform(value) {
this._dirtyTransform = value;
if (value) {
this.dirty = true;
}
}
get dirtyTransform() {
return this._dirtyTransform;
}
set scalingX(value) {
if (this._scalingX !== value) {
this._scalingX = value;
this.dirtyTransform = true;
}
}
get scalingX() {
return this._scalingX;
}
set scalingY(value) {
if (this._scalingY !== value) {
this._scalingY = value;
this.dirtyTransform = true;
}
}
get scalingY() {
return this._scalingY;
}
set scalingCenterX(value) {
if (this._scalingCenterX !== value) {
this._scalingCenterX = value;
this.dirtyTransform = true;
}
}
get scalingCenterX() {
return this._scalingCenterX;
}
set scalingCenterY(value) {
if (this._scalingCenterY !== value) {
this._scalingCenterY = value;
this.dirtyTransform = true;
}
}
get scalingCenterY() {
return this._scalingCenterY;
}
set rotationCenterX(value) {
if (this._rotationCenterX !== value) {
this._rotationCenterX = value;
this.dirtyTransform = true;
}
}
get rotationCenterX() {
return this._rotationCenterX;
}
set rotationCenterY(value) {
if (this._rotationCenterY !== value) {
this._rotationCenterY = value;
this.dirtyTransform = true;
}
}
get rotationCenterY() {
return this._rotationCenterY;
}
set rotation(value) {
if (this._rotation !== value) {
this._rotation = value;
this.dirtyTransform = true;
}
}
get rotation() {
return this._rotation;
}
/**
* 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 rotationDeg(value) {
this.rotation = value / 180 * Math.PI;
}
get rotationDeg() {
return this.rotation / Math.PI * 180;
}
set translationX(value) {
if (this._translationX !== value) {
this._translationX = value;
this.dirtyTransform = true;
}
}
get translationX() {
return this._translationX;
}
set translationY(value) {
if (this._translationY !== value) {
this._translationY = value;
this.dirtyTransform = true;
}
}
get translationY() {
return this._translationY;
}
containsPoint(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
*/
pickNode(x, y) {
if (!this.visible || this.pointerEvents === PointerEvents.None || !this.containsPoint(x, y)) {
return;
}
const 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 (let i = children.length - 1; i >= 0; i--) {
const hit = children[i].pickNode(x, y);
if (hit) {
return hit;
}
}
}
else if (!this.isContainerNode) { // a leaf node, but not a container leaf
return this;
}
}
computeBBox() { return; }
computeBBoxCenter() {
const bbox = this.computeBBox && this.computeBBox();
if (bbox) {
return [
bbox.x + bbox.width * 0.5,
bbox.y + bbox.height * 0.5
];
}
return [0, 0];
}
computeTransformMatrix() {
// 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();
const [bbcx, bbcy] = [0, 0];
const sx = this.scalingX;
const sy = this.scalingY;
let scx;
let 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;
}
const r = this.rotation;
const cos = Math.cos(r);
const sin = Math.sin(r);
let rcx;
let rcy;
if (r === 0) {
rcx = 0;
rcy = 0;
}
else {
rcx = this.rotationCenterX === null ? bbcx : this.rotationCenterX;
rcy = this.rotationCenterY === null ? bbcy : this.rotationCenterY;
}
const tx = this.translationX;
const 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:
const tx4 = scx * (1 - sx) - rcx;
const 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);
}
set dirty(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;
}
}
}
get dirty() {
return this._dirty;
}
set visible(value) {
if (this._visible !== value) {
this._visible = value;
this.dirty = true;
}
}
get visible() {
return this._visible;
}
set zIndex(value) {
if (this._zIndex !== value) {
this._zIndex = value;
if (this.parent) {
this.parent.dirtyZIndex = true;
}
this.dirty = true;
}
}
get zIndex() {
return this._zIndex;
}
}
exports.Node = Node;
Node.MAX_SAFE_INTEGER = Math.pow(2, 53) - 1; // Number.MAX_SAFE_INTEGER
//# sourceMappingURL=node.js.map