UNPKG

highcharts-tree

Version:
249 lines (216 loc) 7.23 kB
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; } }