highcharts-tree
Version:
Create tree type in Highcharts
249 lines (216 loc) • 7.23 kB
text/typescript
import { TreeNodeData } from "../types";
import TreeNode from "./TreeNode";
import Dictionary from "./Dictionary";
export default class Tree<T extends TreeNodeData> {
public static getTree<T extends TreeNodeData>({
tree,
data,
horizontal
}: {
tree?: Tree<T>;
data?: T;
horizontal?: boolean;
}) {
return tree && tree.root ? tree : new Tree(data, horizontal).build();
}
public static rotate<T extends TreeNodeData>(node: TreeNode<T>) {
[node.x, node.y] = [node.y, node.x];
[node.width, node.height] = [node.height, node.width];
if (node.children) node.children.forEach(Tree.rotate);
}
private static nodeSize = 1;
private static siblingDistance = 0.0;
private static treeDistance = 0.0;
private static initializeNodes<T extends TreeNodeData>(
node: TreeNode<T>,
depth: number = 0
) {
node.x = -1;
node.y = depth;
node.mod = 0;
if (!node.toggle) return;
for (const child of node.children) {
Tree.initializeNodes(child, depth + 1);
}
}
private static checkForConflicts<T extends TreeNodeData>(node: TreeNode<T>) {
const nodeContour: Dictionary<number, number> = new Dictionary();
Tree.getLeftContour(node, 0, nodeContour);
let sibling = node.getLeftMostSibling();
let shiftValue = 0.0;
while (sibling != null && sibling !== node) {
const siblingContour: Dictionary<number, number> = new Dictionary();
Tree.getRightContour(sibling, 0, siblingContour);
for (
let level = node.y + 1;
level <=
Math.min(
Math.max.apply(null, siblingContour.keys()),
Math.max.apply(null, nodeContour.keys())
);
level++
) {
const distance =
nodeContour.get(level) - siblingContour.get(level) - Tree.nodeSize;
if (distance + shiftValue < Tree.treeDistance) {
shiftValue = Tree.treeDistance - distance;
}
}
if (shiftValue > 0) {
node.x += shiftValue;
node.mod += shiftValue;
// update the mod in nodeContour
nodeContour.keys(key => {
nodeContour.set(key, nodeContour.get(key) + shiftValue);
});
Tree.centerNodesBetween(node, sibling);
shiftValue = 0;
}
sibling = sibling.getNextSibling();
}
}
private static getLeftContour<T extends TreeNodeData>(
node: TreeNode<T>,
modSum: number,
values: Dictionary<number, number>
) {
if (!values.containsKey(node.y)) values.set(node.y, node.x + modSum);
else {
values.set(node.y, Math.min(values.get(node.y), node.x + modSum));
}
modSum += node.mod;
if (node.toggle) {
for (const child of node.children) {
Tree.getLeftContour(child, modSum, values);
}
}
}
private static getRightContour<T extends TreeNodeData>(
node: TreeNode<T>,
modSum: number,
values: Dictionary<number, number>
) {
if (!values.containsKey(node.y)) values.set(node.y, node.x + modSum);
else {
values.set(node.y, Math.max(values.get(node.y), node.x + modSum));
}
modSum += node.mod;
if (node.toggle) {
for (const child of node.children) {
Tree.getRightContour(child, modSum, values);
}
}
}
private static centerNodesBetween<T extends TreeNodeData>(
leftNode: TreeNode<T>,
rightNode: TreeNode<T>
) {
const leftIndex = leftNode.parent.children.indexOf(rightNode);
const rightIndex = leftNode.parent.children.indexOf(leftNode);
const numNodesBetween = rightIndex - leftIndex - 1;
if (numNodesBetween > 0) {
const distanceBetweenNodes =
(leftNode.x - rightNode.x) / (numNodesBetween + 1);
for (let i = leftIndex + 1; i < rightIndex; i++) {
const middleNode = leftNode.parent.children[i];
const desiredX = rightNode.x + distanceBetweenNodes * (i - leftIndex);
const offset = desiredX - middleNode.x;
middleNode.x += offset;
middleNode.mod += offset;
}
Tree.checkForConflicts(leftNode);
}
}
public root: TreeNode<T>;
private treeMod = 0;
constructor(treeNode: T, public horizontal: boolean = true) {
this.root = this.buildTree(treeNode);
}
public build() {
Tree.initializeNodes(this.root);
this.calculateInitialX(this.root);
this.calculateXWithMod(this.root);
this.calculateFinalPositions(this.root);
if (!this.horizontal) Tree.rotate(this.root);
return this;
}
private calculateInitialX(node: TreeNode<T>) {
if (node.toggle) {
for (const child of node.children) {
this.calculateInitialX(child);
}
}
if (node.isLeaf() || !node.toggle) {
// if there is a previous sibling in this set, set X to previous sibling + designated distance
if (!node.isLeftMost()) {
node.x =
node.getPreviousSibling().x + Tree.nodeSize + Tree.siblingDistance;
} else node.x = 0; // if this is the first node in a set, set X to 0
} else if (node.children.length === 1) {
// if this is the first node in a set, set it's X value equal to it's child's X value
if (node.isLeftMost()) {
node.x = node.children[0].x;
} else {
node.x =
node.getPreviousSibling().x + Tree.nodeSize + Tree.siblingDistance;
node.mod = node.x - node.children[0].x;
}
} else {
const leftChild = node.getLeftMostChild();
const rightChild = node.getRightMostChild();
const mid = (leftChild.x + rightChild.x) / 2;
if (node.isLeftMost()) {
node.x = mid;
} else {
node.x =
node.getPreviousSibling().x + Tree.nodeSize + Tree.siblingDistance;
node.mod = node.x - mid;
}
}
if (node.children.length > 0 && !node.isLeftMost() && node.toggle) {
// Since subtrees can overlap, check for conflicts and shift tree right if needed
Tree.checkForConflicts(node);
}
}
private calculateXWithMod(node: TreeNode<T>, modSum: number = 0) {
node.x += modSum;
if (node.x < 0 && node.x * -1 > this.treeMod) this.treeMod = node.x * -1;
modSum += node.mod;
if (node.toggle) {
for (const child of node.children) {
this.calculateXWithMod(child, modSum);
}
}
}
private calculateFinalPositions(node: TreeNode<T>) {
node.x += this.treeMod;
if (node.toggle) {
for (const child of node.children) {
this.calculateFinalPositions(child);
}
}
if (node.isLeaf() || !node.toggle) {
node.width = node.x;
node.height = node.y;
} else {
const childWidths = node.children
.map(n => n.width)
.sort((p1, p2) => p2 - p1);
const childHeight = node.children
.map(n => n.height)
.sort((p1, p2) => p2 - p1);
node.width = childWidths[0];
node.height = childHeight[0];
}
}
// Build tree as linked list and return root node.
private buildTree(data: T, parent?: TreeNode<T>) {
const currentNode = new TreeNode(data, parent);
currentNode.children = data.children
? data.children.map(childData =>
this.buildTree(childData as T, currentNode)
)
: [];
return currentNode;
}
}