UNPKG

basicprimitives

Version:

Basic Primitives Diagrams for JavaScript - data visualization components library that implements organizational chart and multi-parent dependency diagrams, contains implementations of JavaScript Controls and PDF rendering plugins.

1,466 lines (1,302 loc) 63.8 kB
import { isObject, cloneObject, isEmptyObject } from '../common'; import Graph from './Graph'; import LinkedHashItems from './LinkedHashItems'; /** * Family node * @class FamilyNode * @property {string} id Id * @property {object} node Node */ function FamilyNode(id, node) { this.id = id; this.node = node; } /** * Creates a family object * * @class Family * * @param {Family} [source=undefined] Reference to optional family object to clone properties from * * @returns {Family} Returns new instance of family structure */ export default function Family(source) { var _roots = {}, // children hash of orphant parent id _rootsCount = {}, _children = {}, // children hash by node id _childrenCount = {}, _parents = {}, // parents hash by node id _parentsCount = {}, _nodes = {}, // nodes by node id /** @constant @type {number} @default */ BREAK = 1, /** @constant @type {number} @default */ SKIP = 2; _init(source); function _init(source) { if (isObject(source)) { _roots = cloneObject(source.roots, false); _rootsCount = cloneObject(source.rootsCount, true); _children = cloneObject(source.children, false); _childrenCount = cloneObject(source.childrenCount, true); _parents = cloneObject(source.parents, false); _parentsCount = cloneObject(source.parentsCount, true); _nodes = cloneObject(source.nodes, true); } } function _loop(thisArg, collection, itemid, onItem) { var item, items; if (onItem != null) { items = collection[itemid]; if (items != null) { for (item in items) { if (items.hasOwnProperty(item)) { if (onItem.call(thisArg, item, items[item])) { break; } } } } } } /** * Adds new family member * @param {string[]} parents A collection of parents ids * @param {string} nodeid An id of the new node * @param {object} node A reference to the new node */ function add(parents, nodeid, node) { var index, len, parentid, processed = {}; if (!parents || parents.length === 0) { parents = [null]; } if (_nodes[nodeid] == null && node != null) { _nodes[nodeid] = node; for (index = 0, len = parents.length; index < len; index += 1) { parentid = parents[index]; if (processed[parentid] == null && parentid != nodeid) { processed[parentid] = true; if (_nodes[parentid] != null) { if (_parents[nodeid] == null) { _parents[nodeid] = {}; _parentsCount[nodeid] = 0; } if (!_parents[nodeid][parentid]) { _parents[nodeid][parentid] = true; _parentsCount[nodeid] += 1; } if (_children[parentid] == null) { _children[parentid] = {}; _childrenCount[parentid] = 0; } if (!_children[parentid][nodeid]) { _children[parentid][nodeid] = true; _childrenCount[parentid] += 1; } } else { if (_roots[parentid] == null) { _roots[parentid] = {}; _rootsCount[parentid] = 0; } if (!_roots[parentid][nodeid]) { _roots[parentid][nodeid] = true; _rootsCount[parentid] += 1; } } } } if (_roots[nodeid] != null) { _children[nodeid] = _roots[nodeid]; _childrenCount[nodeid] = _rootsCount[nodeid]; delete _roots[nodeid]; delete _rootsCount[nodeid]; _loop(this, _children, nodeid, function (itemid) { if (_parents[itemid] == null) { _parents[itemid] = {}; _parentsCount[itemid] = 0; } if (!_parents[itemid][nodeid]) { _parents[itemid][nodeid] = true; _parentsCount[itemid] += 1; } }); } } } /** * Returns family node by id * @param {string} nodeid The id of the node * @returns {object|undefined} A reference to the node or undefined if id does not exists */ function node(nodeid) { return _nodes[nodeid]; } /** * Makes node to be a child of every parent in the collection of parents * @param {string[]} parents A collection of parents ids * @param {string} nodeid An id of the new node */ function adopt(parents, nodeid) { var index, len, parentid; if (_nodes[nodeid] != null) { for (index = 0, len = parents.length; index < len; index += 1) { parentid = parents[index]; if (_parents[nodeid] == null) { _parents[nodeid] = {}; _parentsCount[nodeid] = 0; } if (parentid != nodeid && _nodes[parentid] != null) { if (!_parents[nodeid][parentid]) { _parents[nodeid][parentid] = true; _parentsCount[nodeid] += 1; } if (_children[parentid] == null) { _children[parentid] = {}; _childrenCount[parentid] = 0; } if (!_children[parentid][nodeid]) { _children[parentid][nodeid] = true; _childrenCount[parentid] += 1; } } else { throw "Item cannot be parent of itself and parent should exist in the structure!"; } } } else { throw "Child should be in hierarchy!"; } } /** * Removes node * @param {string} nodeid The id of the node */ function removeNode(nodeid) { if (_nodes[nodeid] != null) { _loop(this, _children, nodeid, function (itemid) { delete _parents[itemid][nodeid]; _parentsCount[itemid] -= 1; if (!_parentsCount[itemid]) { delete _parents[itemid]; delete _parentsCount[itemid]; if (_roots[null] == null) { _roots[null] = {}; _rootsCount[null] = 0; } if (!_roots[null][itemid]) { _roots[null][itemid] = true; _rootsCount[null] += 1; } } }); _loop(this, _parents, nodeid, function (itemid) { delete _children[itemid][nodeid]; _childrenCount[itemid] -= 1; if (!_childrenCount[itemid]) { delete _children[itemid]; delete _childrenCount[itemid]; } }); if (_roots[null] != null && _roots[null][nodeid] != null) { delete _roots[null][nodeid]; _rootsCount[null] -= 1; if (!_rootsCount[null]) { delete _roots[null]; delete _rootsCount[null]; } } delete _children[nodeid]; delete _childrenCount[nodeid]; delete _parents[nodeid]; delete _parentsCount[nodeid]; delete _nodes[nodeid]; } } function _removeChildReference(parentid, childid) { var result = false; if (_children[parentid] != null && _children[parentid][childid] != null) { delete _children[parentid][childid]; _childrenCount[parentid] -= 1; delete _parents[childid][parentid]; _parentsCount[childid] -= 1; if (!_childrenCount[parentid]) { delete _children[parentid]; delete _childrenCount[parentid]; } if (!_parents[childid]) { delete _parents[childid]; delete _parentsCount[childid]; if (_roots[null] == null) { _roots[null] = {}; _rootsCount[null] = 0; } _roots[null][childid] = true; _rootsCount[null] += 1; } result = true; } return result; } /** * Removes first available parent child or child parent relation * * @param {string} fromid From node id * @param {string} toid To node id * @returns {true} If relation was broken */ function removeRelation(fromid, toid) { var result = false; if (_nodes[fromid] != null && _nodes[toid] != null) { result = _removeChildReference(fromid, toid) || _removeChildReference(toid, fromid); } return result; } /** * Removes child relation * * @param {string} parentid The parent node id * @param {string} childid The child node id * @returns {true} If relation was broken */ function removeChildRelation(parentid, childid) { var result = false; if (_nodes[parentid] != null && _nodes[childid] != null) { result = _removeChildReference(parentid, childid); } return result; } /** * Returns true if structure has nodes. * * @returns {boolean} Returns true if family structure has nodes */ function hasNodes() { return !isEmptyObject(_nodes); } /** * Callback for iterating family nodes * * @callback onFamilyItemCallback * @param {string} itemid The node id * @param {object} item The node * @returns {boolean} Returns true to break the loop */ /** * Loops through nodes of family structure * * @param {Object} thisArg The callback function invocation context * @param {onFamilyItemCallback} onItem A callback function to call for every family node */ function loop(thisArg, onItem) { var item; if (onItem != null) { for (item in _nodes) { if (_nodes.hasOwnProperty(item)) { if (onItem.call(thisArg, item, _nodes[item])) { break; } } } } } function _loopItems(thisArg, collection, items, onItem) { // onItem(itemid, item, levelIndex) var newItems, itemid, processed = {}, levelIndex = 0, hasItems = true; while (hasItems) { newItems = {}; hasItems = false; for (itemid in items) { if (items.hasOwnProperty(itemid)) { if (!processed[itemid]) { processed[itemid] = true; switch (onItem.call(thisArg, itemid, _nodes[itemid], levelIndex)) { case 1/*BREAK*/: newItems = {}; hasItems = false; break; case 2/*SKIP*/: break; default: _loop(this, collection, itemid, function (newItemId) { if (!processed[newItemId]) { newItems[newItemId] = true; hasItems = true; } }); //ignore jslint break; } } } } items = newItems; levelIndex += 1; } } /** * Callback for iterating family nodes level by level * * @callback onFamilyItemWithLevelCallback * @param {string} itemid The node id * @param {object} item The node * @param {number} levelIndex The node level index * @returns {number} Returns BREAK to break the loop and exit. Returns SKIP to skip node's branch traversing. */ /** * Loops through child nodes of family structure level by level * * @param {Object} thisArg The callback function invocation context * @param {string} nodeid The node id to start children traversing * @param {onFamilyItemWithLevelCallback} onItem A callback function to call for every child node */ function loopChildren(thisArg, nodeid, onItem) { if (onItem != null) { if (nodeid != null && _nodes[nodeid] != null && _children[nodeid] != null) { _loopItems(thisArg, _children, _children[nodeid], onItem); } } } /** * Loops through parent nodes of family structure level by level * * @param {Object} thisArg The callback function invocation context * @param {string} nodeid The node id to start parents traversing * @param {onFamilyItemWithLevelCallback} onItem A callback function to call for every parent node */ function loopParents(thisArg, nodeid, onItem) { if (onItem != null) { if (nodeid != null && _nodes[nodeid] != null && _parents[nodeid] != null) { _loopItems(thisArg, _parents, _parents[nodeid], onItem); } } } function _loopTopo(thisArg, backwardCol, backwardCount, forwardCol, forwardCount, onItem) { // onItem(itemid, item, position) var index, len, nodeid, references, queue, newQueue, position; if (onItem != null) { /* count parents for every node */ queue = []; references = {}; for (nodeid in _nodes) { if (_nodes.hasOwnProperty(nodeid)) { references[nodeid] = (backwardCount[nodeid] || 0); if (!references[nodeid]) { queue.push(nodeid); } } } /* iterate queue and reduce reference counts via children */ position = 0; while (queue.length > 0) { newQueue = []; for (index = 0, len = queue.length; index < len; index += 1) { nodeid = queue[index]; if (onItem.call(thisArg, nodeid, _nodes[nodeid], position)) { newQueue = []; break; } position += 1; _loop(this, forwardCol, nodeid, function (itemid) { references[itemid] -= 1; if (references[itemid] === 0) { newQueue.push(itemid); } }); //ignore jslint } queue = newQueue; } } } /** * Callback for iterating family nodes in topological sort order * * @callback onFamilyTopoCallback * @param {string} itemid The node id * @param {object} item The node * @param {number} position The node position in the sequence * @returns {boolean} Returns true to break the loop and exit. */ /** * Loops through topologically sorted nodes of family structure * * @param {Object} thisArg The callback function invocation context * @param {onFamilyTopoCallback} onItem A callback function to call for every node */ function loopTopo(thisArg, onItem) { _loopTopo(thisArg, _parents, _parentsCount, _children, _childrenCount, onItem); } /** * Loops through reversed order topologically sorted nodes of family structure * * @param {Object} thisArg The callback function invocation context * @param {onFamilyTopoCallback} onItem A callback function to call for every node */ function loopTopoReversed(thisArg, onItem) { _loopTopo(thisArg, _children, _childrenCount, _parents, _parentsCount, onItem); } /** * Loops through nodes of family structure level by level. This function aligns nodes top or bottom. * * @param {Object} thisArg The callback function invocation context * @param {boolean} parentAligned True if nodes should be placed at the next level after their parents level, * otherwise nodes placed at levels close to their children. * @param {onFamilyItemWithLevelCallback} onItem A callback function to call for every node */ function loopLevels(thisArg, parentAligned, onItem) { var topoSorted = [], topoSortedPositions = {}, processed = {}, margin = [], /* result items distribution by levels */ levels = {}, levelIndex, groups = {}, hasGroups, newGroups, groupIndex, group, itemsAtLevel, itemid, minimumLevel = null, loopFunc = parentAligned ? loopTopo : loopTopoReversed, index, len, mIndex, mLen, mItem, mLevel, topoSortedItem, bestPosition, bestItem, bestLevel, bestIsParent, newMargin, hasNeighbours; function Group() { this.items = {}; this.minimumLevel = null; } Group.prototype.addItemToLevel = function (itemid, level) { var items = this.items[level]; if (!items) { items = [itemid]; this.items[level] = items; } else { items.push(itemid); } this.minimumLevel = this.minimumLevel == null ? level : Math.min(this.minimumLevel, level); }; function addItemToLevel(itemid, index, level) { var group = groups[index]; if (!group) { group = new Group(); groups[index] = group; } group.addItemToLevel(itemid, level); minimumLevel = minimumLevel == null ? level : Math.min(minimumLevel, level); levels[itemid] = level; processed[itemid] = true; } if (onItem != null) { /* sort items topologically */ loopFunc(this, function (itemid, item, position) { topoSorted.push(itemid); topoSortedPositions[itemid] = position; }); /* search for the first available non processed item in topological order */ for (index = 0, len = topoSorted.length; index < len; index += 1) { topoSortedItem = topoSorted[index]; if (processed[topoSortedItem] == null) { margin.push(topoSortedItem); addItemToLevel(topoSortedItem, index, 0); /* use regular graph breadth first search */ while (margin.length > 0) { bestPosition = null; bestItem = null; bestLevel = null; bestIsParent = !parentAligned; newMargin = []; for (mIndex = 0, mLen = margin.length; mIndex < mLen; mIndex += 1) { mItem = margin[mIndex]; mLevel = levels[mItem]; hasNeighbours = false; if (parentAligned) { _loop(this, _parents, mItem, function (parentid) { var topoSortedPosition; if (!processed[parentid]) { hasNeighbours = true; topoSortedPosition = topoSortedPositions[parentid]; if (bestPosition == null || !bestIsParent || bestPosition < topoSortedPosition || (bestPosition == topoSortedPosition && bestLevel > mLevel - 1)) { bestPosition = topoSortedPosition; bestItem = parentid; bestLevel = mLevel - 1; bestIsParent = true; } } }); //ignore jslint _loop(this, _children, mItem, function (childid) { var topoSortedPosition; if (!processed[childid]) { hasNeighbours = true; topoSortedPosition = topoSortedPositions[childid]; if (bestPosition == null || (!bestIsParent && (bestPosition > topoSortedPosition || (bestPosition == topoSortedPosition && bestLevel < mLevel + 1)))) { bestPosition = topoSortedPosition; bestItem = childid; bestLevel = mLevel + 1; bestIsParent = false; } } }); //ignore jslint } else { _loop(this, _children, mItem, function (childid) { var topoSortedPosition; if (!processed[childid]) { hasNeighbours = true; topoSortedPosition = topoSortedPositions[childid]; if (bestPosition == null || bestIsParent || bestPosition < topoSortedPosition || (bestPosition == topoSortedPosition && bestLevel < mLevel + 1)) { bestPosition = topoSortedPosition; bestItem = childid; bestLevel = mLevel + 1; bestIsParent = false; } } }); //ignore jslint _loop(this, _parents, mItem, function (parentid) { var topoSortedPosition; if (!processed[parentid]) { hasNeighbours = true; topoSortedPosition = topoSortedPositions[parentid]; if (bestPosition == null || (bestIsParent && (bestPosition > topoSortedPosition || (bestPosition == topoSortedPosition && bestLevel > mLevel - 1)))) { bestPosition = topoSortedPosition; bestItem = parentid; bestLevel = mLevel - 1; bestIsParent = true; } } }); //ignore jslint } if (hasNeighbours) { newMargin.push(mItem); } } if (bestItem != null) { newMargin.push(bestItem); addItemToLevel(bestItem, index, bestLevel); } margin = newMargin; } } } hasGroups = true; levelIndex = minimumLevel; while (hasGroups) { newGroups = {}; hasGroups = false; for (groupIndex in groups) { if (groups.hasOwnProperty(groupIndex)) { group = groups[groupIndex]; itemsAtLevel = group.items[(group.minimumLevel - minimumLevel) + levelIndex]; if (itemsAtLevel != null) { newGroups[groupIndex] = group; hasGroups = true; for (index = 0, len = itemsAtLevel.length; index < len; index += 1) { itemid = itemsAtLevel[index]; if (onItem.call(thisArg, itemid, _nodes[itemid], levelIndex - minimumLevel)) { hasGroups = false; return true; } } } } } groups = newGroups; levelIndex += 1; } } } /** * Loops root nodes of family structure. * @param {Object} thisArg The callback function invocation context * @param {onFamilyItemCallback} onItem A callback function to call for every family root node */ function loopRoots(thisArg, onItem) { var result = null, minimum, counter = 0, famMembers = {}, famCount = {}, isRoot, roots = {}, processed = {}, famItemId, member, members, rootid, membersRoots, memberRoots, memberRoot, index, len; loopTopoReversed(this, function (famItemId, famItem, position) { /* every node has at least itself in members */ if (!famMembers.hasOwnProperty(famItemId)) { famMembers[famItemId] = {}; famCount[famItemId] = 0; } famMembers[famItemId][famItemId] = true; famCount[famItemId] += 1; isRoot = true; loopParents(this, famItem.id, function (parentid, parent, levelIndex) { var items, itemid; isRoot = false; if (!famMembers.hasOwnProperty(parentid)) { famMembers[parentid] = {}; famCount[parentid] = 0; } /* push famItem members to parent members collection */ if (!famCount[parentid] && _parentsCount[famItemId] == 1) { famMembers[parentid] = famMembers[famItemId]; famCount[parentid] = famCount[famItemId]; } else { items = famMembers[famItemId]; for (itemid in items) { if (items.hasOwnProperty(itemid)) { if (!famMembers[parentid][itemid]) { famMembers[parentid][itemid] = true; famCount[parentid] += 1; } } } } return SKIP; }); if (isRoot) { roots[famItemId] = true; counter += 1; } }); /* create collection of roots per member */ membersRoots = {}; for (rootid in roots) { if (roots.hasOwnProperty(rootid)) { members = famMembers[rootid]; for (member in members) { if (members.hasOwnProperty(member)) { if (!membersRoots[member]) { membersRoots[member] = []; } membersRoots[member].push(rootid.toString()); } } } } /* loop minimal sub tree roots */ while (counter > 0) { minimum = null; for (famItemId in roots) { if (roots.hasOwnProperty(famItemId)) { if (!minimum || famCount[famItemId] < minimum) { minimum = famCount[famItemId]; result = famItemId; } } } if (result != null) { if (onItem != null) { onItem.call(thisArg, result, _nodes[result]); } members = famMembers[result]; for (member in members) { if (members.hasOwnProperty(member)) { if (!processed[member]) { memberRoots = membersRoots[member]; for (index = 0, len = memberRoots.length; index < len; index += 1) { memberRoot = memberRoots[index]; famCount[memberRoot] -= 1; } processed[member] = true; } } } delete roots[result]; counter -= 1; } } } /** * Finds root node having largest number of nodes in its hierarchy * * @returns {string} Returns largest sub-hierarchy root node id. */ function findLargestRoot() { var result = null, maximum, famMembers = {}, famCount = {}, isRoot; maximum = null; loopTopoReversed(this, function (famItemId, famItem, position) { /* every node has at least itself in members */ if (!famMembers.hasOwnProperty(famItemId)) { famMembers[famItemId] = {}; famCount[famItemId] = 0; } famMembers[famItemId][famItemId] = true; famCount[famItemId] += 1; isRoot = true; loopParents(this, famItem.id, function (parentid, parent, levelIndex) { var items, itemid; isRoot = false; if (!famMembers.hasOwnProperty(parentid)) { famMembers[parentid] = {}; famCount[parentid] = 0; } /* push famItem members to parent members collection */ if (!famCount[parentid] && _parentsCount[famItemId] == 1) { famMembers[parentid] = famMembers[famItemId]; famCount[parentid] = famCount[famItemId]; } else { items = famMembers[famItemId]; for (itemid in items) { if (items.hasOwnProperty(itemid)) { famMembers[parentid][itemid] = true; famCount[parentid] += 1; } } } return SKIP; }); if (isRoot && (!maximum || famCount[famItemId] > maximum)) { maximum = famCount[famItemId]; result = famItemId; } }); return result; } /** * Checks whether parents share a child node. Common child should belong only to the given collection * of parents, if child's parents don't match given collection of parents, * it is not considered as common child. * @param {string[]} parents Collection of parents * @returns {boolean} Returns true if common child exist. */ function hasCommonChild(parents) { var result = false, parentsHash, childrenHash, parentsCount, pIndex, pLen, parent, child; /* convert parents collection to hash, remove duplicates and ignore non-existing items */ parentsHash = {}; parentsCount = 0; for (pIndex = 0, pLen = parents.length; pIndex < pLen; pIndex += 1) { parent = parents[pIndex]; if (_nodes[parent] != null && !parentsHash[parent]) { parentsHash[parent] = true; parentsCount += 1; } } /* collect number of parents referencing each child */ childrenHash = {}; for (parent in parentsHash) { if (parentsHash.hasOwnProperty(parent)) { _loop(this, _children, parent, function (child) { if (!childrenHash[child]) { childrenHash[child] = 1; } else { childrenHash[child] += 1; } }); //ignore jslint } } /* find common child having number of references equal to number of existing parents */ for (child in childrenHash) { if (childrenHash.hasOwnProperty(child)) { if (_parents[child] != null && (_parentsCount[child] || 0) == childrenHash[child] && childrenHash[child] == parentsCount) { result = true; break; } } } return result; } function _bundleNodes(fromItem, items, bundleItemId, bundleItem, backwardCol, backwardCount, forwardCol, forwardCount, checkChildren) { var isValid = false, index, len, child; if (_nodes[fromItem] != null && forwardCol[fromItem] != null) { /* validate target items */ isValid = true; if (checkChildren) { /* if we add new bundle all items should present */ for (index = 0, len = items.length; index < len; index += 1) { child = items[index]; if (_nodes[child] == null || forwardCol[fromItem][child] == null) { isValid = false; } } } if (isValid) { if (bundleItem != null) { /* add bundle node */ _nodes[bundleItemId] = bundleItem; } if (_nodes[bundleItemId] != null) { /* update references */ if (!backwardCol[bundleItemId]) { backwardCol[bundleItemId] = {}; backwardCount[bundleItemId] = 0; } if (!forwardCol[bundleItemId]) { forwardCol[bundleItemId] = {}; forwardCount[bundleItemId] = 0; } if (!backwardCol[bundleItemId][fromItem]) { backwardCol[bundleItemId][fromItem] = true; backwardCount[bundleItemId] += 1; } if (!forwardCol[fromItem][bundleItemId]) { forwardCol[fromItem][bundleItemId] = true; forwardCount[fromItem] += 1; } for (index = 0, len = items.length; index < len; index += 1) { child = items[index]; if (bundleItemId != child) { if (forwardCol[fromItem][child] != null) { delete forwardCol[fromItem][child]; forwardCount[fromItem] -= 1; } if (backwardCol[child][fromItem] != null) { delete backwardCol[child][fromItem]; backwardCount[child] -= 1; } if (!backwardCol[child][bundleItemId]) { backwardCol[child][bundleItemId] = true; backwardCount[child] += 1; } if (!forwardCol[bundleItemId][child]) { forwardCol[bundleItemId][child] = true; forwardCount[bundleItemId] += 1; } } } } } } return isValid; } /** * Adds extra bundle item in between parent and its children. The parent node becomes parent of the bundle node, * and bundle becomes parent of the children. Existing parent child relations are removed. * @param {string} parent The parent node id * @param {string[]} children The collection of child nodes ids * @param {string} bundleItemId The bundle node id * @param {object} bundleItem The bundle item context object * @returns {boolean} Returns true if nodes bundle is valid */ function bundleChildren(parent, children, bundleItemId, bundleItem) { return _bundleNodes(parent, children, bundleItemId, bundleItem, _parents, _parentsCount, _children, _childrenCount, true); } /** * Adds extra bundle item in between child node and its parents. The child node becomes child of the bundle node, * and bundle becomes child of the parents. Existing parent child relations are removed. * @param {string} child The parent node id * @param {string[]} parents The collection of child nodes ids * @param {string} bundleItemId The bundle node id * @param {object} bundleItem The bundle item context object * @returns {boolean} Returns true if the bundle is valid */ function bundleParents(child, parents, bundleItemId, bundleItem) { return _bundleNodes(child, parents, bundleItemId, bundleItem, _children, _childrenCount, _parents, _parentsCount, true); } function ReferenceItem() { this.id = ""; this.key = ""; this.children = []; this.childrenHash = {}; this.processed = false; } function ReferencesEdge(arg0) { this.items = []; this.weight = 0; this.difference = 0; if (arguments.length > 0) { this.difference = arg0; } } function _getReferencesGraph(currentItems) { var result = Graph(), item, parents, index1, index2, len, from, to, difference, processed = {}; for (item in currentItems) { if (currentItems.hasOwnProperty(item)) { _loop(this, _children, item, function (child) { if (!processed.hasOwnProperty(child)) { processed[child] = true; /* create array of parents from hash references */ parents = []; _loop(this, _parents, child, function (parent) { parents.push(parent); }); /* create all possible combinations between items */ for (index1 = 0, len = parents.length; index1 < len - 1; index1 += 1) { from = parents[index1]; if (currentItems.hasOwnProperty(from)) { for (index2 = index1 + 1; index2 < len; index2 += 1) { to = parents[index2]; if (currentItems.hasOwnProperty(to)) { difference = Math.abs(currentItems[from].children.length - currentItems[to].children.length); var edge = result.edge(from, to); if (edge == null) { edge = new ReferencesEdge(difference); result.addEdge(from, to, edge); } edge.items.push(child); edge.weight += 1; } } } } } }); //ignore jslint } } return result; } /** * Callback function for creation of new family nodes * * @callback onNewFamilyNodeCallback * @returns {object} Returns new family node. */ /** * Optimizes references between family members. * It creates bundles eliminating excessive intersections between nodes relations. * * @param {onNewFamilyNodeCallback} onNewBundleItem Callback function to create a new family node context object. */ function optimizeReferences(onNewBundleItem) { var sharedItemsByKey = {}, sharedItemsById = {}, currentItems = {}, nodeid, newReferenceItem, nextItems, graph, node, maximumTree, counter = 0, power = 10, processed; if (onNewBundleItem != null) { for (nodeid in _nodes) { counter += 1; if (_nodes.hasOwnProperty(nodeid)) { newReferenceItem = new ReferenceItem(); _loop(this, _children, nodeid, function (child) { newReferenceItem.children.push(child); newReferenceItem.childrenHash[child] = true; }); //ignore jslint newReferenceItem.children.sort(); newReferenceItem.id = nodeid; newReferenceItem.key = newReferenceItem.children.join(","); currentItems[newReferenceItem.id] = newReferenceItem; } } power = Math.pow(10, (counter).toString().length); while (!isEmptyObject(currentItems)) { nextItems = {}; processed = {}; graph = _getReferencesGraph(currentItems); for (nodeid in currentItems) { if (currentItems.hasOwnProperty(nodeid)) { node = currentItems[nodeid]; if (!node.processed) { maximumTree = graph.getSpanningTree(nodeid, function (edge) { return edge.weight * power + power - edge.difference; }); //ignore jslint maximumTree.loopLevels(this, function (treeKey, treeKeyNode, levelid) { currentItems[treeKey].processed = true; maximumTree.loopChildren(this, treeKey, function (child, childNode) { var relation = graph.edge(treeKey, child), nextBundleItem = null, newItem, key, index, len, childrenToBind, isSharedItem, relationItem; currentItems[child].processed = true; if (relation.weight > 1) { relation.items.sort(); key = relation.items.join(','); if (!sharedItemsByKey.hasOwnProperty(key)) { newItem = onNewBundleItem(); _nodes[newItem.id] = newItem; /* add new bundle node to the family */ nextBundleItem = new ReferenceItem(); nextBundleItem.id = newItem.id; nextBundleItem.key = key; for (index = 0, len = relation.items.length; index < len; index += 1) { relationItem = relation.items[index]; nextBundleItem.children.push(relationItem); nextBundleItem.childrenHash[relationItem] = true; processed[relationItem] = true; } nextBundleItem.children.sort(); sharedItemsByKey[nextBundleItem.key] = nextBundleItem; sharedItemsById[nextBundleItem.id] = nextBundleItem; nextItems[nextBundleItem.id] = nextBundleItem; processed[nextBundleItem.id] = nextBundleItem; childrenToBind = nextBundleItem.children.slice(0); loopChildren(this, treeKeyNode.replacementItem || treeKey, function (childid, child, level) { // if child item is bundle and it is not child of new bundle item if (!nextBundleItem.childrenHash[childid] && sharedItemsById[childid] != null) { isSharedItem = true; // if all children of that child are in the next bundle item we add it to that new bundle item as well loopChildren(this, childid, function (childid, child, level) { if (!nextBundleItem.childrenHash[childid]) { isSharedItem = false; return 1/*BREAK*/; } if (!processed.hasOwnProperty(childid)) { return SKIP; } }); if (isSharedItem) { childrenToBind.push(childid); } } return 2/*SKIP*/; }); _bundleNodes(treeKeyNode.replacementItem || treeKey, childrenToBind, nextBundleItem.id, newItem, _parents, _parentsCount, _children, _childrenCount, false); if ((_childrenCount[treeKey] || 0) <= 1 && treeKeyNode.replacementItem == null) { treeKeyNode.replacementItem = nextBundleItem.id; } } else { nextBundleItem = sharedItemsByKey[key]; } /* don't add shared item to itself on next items loop*/ if (nextBundleItem.id != child) { childrenToBind = nextBundleItem.children.slice(0); loopChildren(this, childNode.replacementItem || child, function (childid, child, level) { if (sharedItemsById[childid] != null && !nextBundleItem.childrenHash[childid]) { isSharedItem = true; loopChildren(this, childid, function (childid, child, level) { if (!nextBundleItem.childrenHash[childid]) { isSharedItem = false; return 1/*BREAK*/; } if (!processed.hasOwnProperty(childid)) { return 2/*SKIP*/; } return SKIP; }); if (isSharedItem) { childrenToBind.push(childid); } } return 2/*SKIP*/; }); _bundleNodes(childNode.replacementItem || child, childrenToBind, nextBundleItem.id, null, _parents, _parentsCount, _children, _childrenCount, false); /* if all items bundled then use bundle item for following transformations of references instead of original item if references graph*/ if ((_childrenCount[child] || 0) <= 1 && childNode.replacementItem == null) { childNode.replacementItem = nextBundleItem.id; } } } }); }); //ignore jslint } } } currentItems = nextItems; } } } /** * Eliminates many to many relations in family structure * It is needed to simplify layout process of the diagram * * @param {onNewFamilyNodeCallback} onNewBundleItem Callback function for creation of new bundle node */ function eliminateManyToMany(onNewBundleItem) { var parent, bundleNode; for (parent in _children) { if (_children.hasOwnProperty(parent)) { if ((_childrenCount[parent] || 0) > 1) { _loop(this, _children, parent, function (child) { if ((_parentsCount[child] || 0) > 1) { bundleNode = onNewBundleItem(); bundleChildren(parent, [child], bundleNode.id, bundleNode); } }); //ignore jslint } } } } function FamilyEdge(parentid, childid) { this.parentid = parentid; this.childid = childid; this.key = parentid + "," + childid; } /** * Eliminates crossing parent child relations between nodes based of nodes order in treeLevels structure. * @param {treeLevels} treeLevels Tree levels structure keeps orders of nodes level by level. * @returns {family} Returns planar family structure. */ function getPlanarFamily(treeLevels) { var result = new Family(), familyEdgeIndex, familyEdgeLen, familyEdgeKey; treeLevels.loopLevels(this, function (levelIndex, treeLevel) { var sequence = new LinkedHashItems(), crossings = {}, familyEdges = {}, firstBucket = []; treeLevels.loopLevelItems(this, levelIndex, function (parentid, parentItem, position) { loopChildren(this, parentid, function (childid, childItem) { var childPosition = treeLevels.getItemPosition(childid); var familyEdge = new FamilyEdge(parentid, childid); familyEdges[familyEdge.key] = familyEdge; var crossEdges = []; if (sequence.isEmpty()) { sequence.add(childPosition, [familyEdge]); } else { sequence.iterateBack(function (sequenceItem, itemPosition) { if (itemPosition < childPosition) { // add new sequence after itemPosition and exit sequence.insertAfter(itemPosition, childPosition, [familyEdge]); return true; } else if (itemPosition == childPosition) { // add new link to existing sequenceItem and exit sequenceItem.push(familyEdge); return true; } else { // merge links into output for (var crossEdgesIndex = 0, crossEdgesLen = sequenceItem.length; crossEdgesIndex < crossEdgesLen; crossEdgesIndex += 1) { var crossEdge = sequenceItem[crossEdgesIndex]; if (crossEdge.parentid != parentid) { crossEdges.push(crossEdge); } } } }); if (sequence.startKey() > childPosition) { sequence.unshift(childPosition, [familyEdge]); } } crossings[familyEdge.key] = crossEdges; for (var crossEdgesIndex = 0, crossEdgesLen = crossEdges.length; crossEdgesIndex < crossEdgesLen; crossEdgesIndex += 1) { crossings[crossEdges[crossEdgesIndex].key].push(familyEdge); } return SKIP; }); if (countChildren(parentid) == 1) { var childid = firstChild(parentid); if (countParents(childid) == 1) { var familyEdge = new FamilyEdge(parentid, childid); firstBucket.push(familyEdge.key); } } }); // distribute edges by number of crossings into buckets var buckets = [], crossEdges; for (var familyEdgeKey in crossings) { crossEdges = crossings[familyEdgeKey]; var len = crossEdges.length; if (buckets[len] != null) { buckets[len].push(familyEdgeKey); } else { buckets[len] = [familyEdgeKey]; } } var processed = {}; // leave single parent child relations buckets.unshift(firstBucket); // break relations having for (var bucketIndex = 0, bucketsLen = buckets.length; bucketIndex < bucketsLen; bucketIndex += 1) { var bucket = buckets[bucketIndex]; if (bucket != null) { for (familyEdgeIndex = 0, familyEdgeLen = bucket.length; familyEdgeIndex < familyEdgeLen; familyEdgeIndex += 1) { familyEdgeKey = bucket[familyEdgeIndex]; if (!processed.hasOwnProperty(familyEdgeKey)) { processed[familyEdgeKey] = true; var familyEdge = familyEdges[familyEdgeKey]; if (result.node(familyEdge.parentid) == null) { result.add(null, familyEdge.parentid, {}); } if (result.node(familyEdge.childid) == null) { result.add([familyEdge.parentid], familyEdge.childid, {}); } else { result.adopt([familyEdge.parentid], familyEdge.childid); } crossEdges = crossings[familyEdgeKey]; for (var crossEdgesIndex = 0, crossEdgesLen = crossEdges.length; crossEdgesIndex < crossEdgesLen; crossEdgesIndex += 1) { processed[crossEdges[crossEdgesIndex].key] = true; } } } } } }); return result; } function Link(from, to, distance) { this.from = from; this.to = to; this.distance = 0; } /** * Eliminates direct relations between grand parent nodes. * * @returns {family} Returns family structure without direct grand parent relations. */ function getFamilyWithoutGrandParentsRelations() { var result = new Family(); var hash = {}; var links = []; var level = 0; for (var from in _parents) { if (_parents.hasOwnProperty(from)) { _loop(this, _parents, from, function (to) { var fromHash = hash[from]; if (fromHash == null) { fromHash = {}; hash[from] = fromHash; } if (!fromHash.hasOwnProperty(to)) { var link = new Link(from, to, level); links.push(link); hash[from][to] = link; } }); //ignore jslint } } while (links.length > 0) { var newLinks = []; level += 1; for (var index = 0, len = links.length; index < len; index += 1) { var link = links[index]; from = link.to; if (_parents.hasOwnProperty(from)) { _loop(this, _parents, from, function (to) { var fromHash = hash[link.from]; if (fromHash == null) { fromHash = {}; hash[link.from] = fromHash; } if (fromHash.hasOwnProperty(to)) { fromHash[to].distance = level; } else { var newLink = new Link(from, to, level); newLinks.push(newLink); fromHash[to] = newLink; } }); } } links = newLinks; } // return only references to immidiate parents loop(this, function (nodeid, node) { var parents = []; _loop(this, _parents, nodeid, function (to) { if (hash[nodeid][to].distance === 0) { parents.push(to); } }); result.add(parents, nodeid, node); }); return result; } /** * Returns number of children * @param {string} parent The parent node id * @returns {number} Number of children */ function