UNPKG

@progress/kendo-ui

Version:

This package is part of the [Kendo UI for jQuery](http://www.telerik.com/kendo-ui) suite.

1,314 lines (1,156 loc) 151 kB
module.exports = /******/ (function(modules) { // webpackBootstrap /******/ // The module cache /******/ var installedModules = {}; /******/ // The require function /******/ function __webpack_require__(moduleId) { /******/ // Check if module is in cache /******/ if(installedModules[moduleId]) /******/ return installedModules[moduleId].exports; /******/ // Create a new module (and put it into the cache) /******/ var module = installedModules[moduleId] = { /******/ exports: {}, /******/ id: moduleId, /******/ loaded: false /******/ }; /******/ // Execute the module function /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); /******/ // Flag the module as loaded /******/ module.loaded = true; /******/ // Return the exports of the module /******/ return module.exports; /******/ } /******/ // expose the modules object (__webpack_modules__) /******/ __webpack_require__.m = modules; /******/ // expose the module cache /******/ __webpack_require__.c = installedModules; /******/ // __webpack_public_path__ /******/ __webpack_require__.p = ""; /******/ // Load entry module and return exports /******/ return __webpack_require__(0); /******/ }) /************************************************************************/ /******/ ({ /***/ 0: /***/ (function(module, exports, __webpack_require__) { module.exports = __webpack_require__(876); /***/ }), /***/ 3: /***/ (function(module, exports) { module.exports = function() { throw new Error("define cannot be used indirect"); }; /***/ }), /***/ 876: /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;(function(f, define){ !(__WEBPACK_AMD_DEFINE_ARRAY__ = [ __webpack_require__(877) ], __WEBPACK_AMD_DEFINE_FACTORY__ = (f), __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); })(function(){ (function ($, undefined) { var kendo = window.kendo, diagram = kendo.dataviz.diagram, Graph = diagram.Graph, Node = diagram.Node, Link = diagram.Link, deepExtend = kendo.deepExtend, Size = diagram.Size, Rect = diagram.Rect, Dictionary = diagram.Dictionary, Set = diagram.Set, HyperTree = diagram.Graph, Utils = diagram.Utils, Point = diagram.Point, EPSILON = 1e-06, DEG_TO_RAD = Math.PI / 180, contains = Utils.contains, grep = $.grep; /** * Base class for layout algorithms. * @type {*} */ var LayoutBase = kendo.Class.extend({ defaultOptions: { type: "Tree", subtype: "Down", roots: null, animate: false, //------------------------------------------------------------------- /** * Force-directed option: whether the motion of the nodes should be limited by the boundaries of the diagram surface. */ limitToView: false, /** * Force-directed option: the amount of friction applied to the motion of the nodes. */ friction: 0.9, /** * Force-directed option: the optimal distance between nodes (minimum energy). */ nodeDistance: 50, /** * Force-directed option: the number of time things are being calculated. */ iterations: 300, //------------------------------------------------------------------- /** * Tree option: the separation in one direction (depends on the subtype what direction this is). */ horizontalSeparation: 90, /** * Tree option: the separation in the complementary direction (depends on the subtype what direction this is). */ verticalSeparation: 50, //------------------------------------------------------------------- /** * Tip-over tree option: children-to-parent vertical distance. */ underneathVerticalTopOffset: 15, /** * Tip-over tree option: children-to-parent horizontal distance. */ underneathHorizontalOffset: 15, /** * Tip-over tree option: leaf-to-next-branch vertical distance. */ underneathVerticalSeparation: 15, //------------------------------------------------------------------- /** * Settings object to organize the different components of the diagram in a grid layout structure */ grid: { /** * The width of the grid in which components are arranged. Beyond this width a component will be on the next row. */ width: 1500, /** * The left offset of the grid. */ offsetX: 50, /** * The top offset of the grid. */ offsetY: 50, /** * The horizontal padding within a cell of the grid where a single component resides. */ componentSpacingX: 20, /** * The vertical padding within a cell of the grid where a single component resides. */ componentSpacingY: 20 }, //------------------------------------------------------------------- /** * Layered option: the separation height/width between the layers. */ layerSeparation: 50, /** * Layered option: how many rounds of shifting and fine-tuning. */ layeredIterations: 2, /** * Tree-radial option: the angle at which the layout starts. */ startRadialAngle: 0, /** * Tree-radial option: the angle at which the layout starts. */ endRadialAngle: 360, /** * Tree-radial option: the separation between levels. */ radialSeparation: 150, /** * Tree-radial option: the separation between the root and the first level. */ radialFirstLevelSeparation: 200, /** * Tree-radial option: whether a virtual roots bing the components in one radial layout. */ keepComponentsInOneRadialLayout: false, //------------------------------------------------------------------- // TODO: ensure to change this to false when containers are around ignoreContainers: true, layoutContainerChildren: false, ignoreInvisible: true, animateTransitions: false }, init: function () { }, /** * Organizes the components in a grid. * Returns the final set of nodes (not the Graph). * @param components */ gridLayoutComponents: function (components) { if (!components) { throw "No components supplied."; } // calculate and cache the bounds of the components Utils.forEach(components, function (c) { c.calcBounds(); }); // order by decreasing width components.sort(function (a, b) { return b.bounds.width - a.bounds.width; }); var maxWidth = this.options.grid.width, offsetX = this.options.grid.componentSpacingX, offsetY = this.options.grid.componentSpacingY, height = 0, startX = this.options.grid.offsetX, startY = this.options.grid.offsetY, x = startX, y = startY, i, resultLinkSet = [], resultNodeSet = []; while (components.length > 0) { if (x >= maxWidth) { // start a new row x = startX; y += height + offsetY; // reset the row height height = 0; } var component = components.pop(); this.moveToOffset(component, new Point(x, y)); for (i = 0; i < component.nodes.length; i++) { resultNodeSet.push(component.nodes[i]); // to be returned in the end } for (i = 0; i < component.links.length; i++) { resultLinkSet.push(component.links[i]); } var boundingRect = component.bounds; var currentHeight = boundingRect.height; if (currentHeight <= 0 || isNaN(currentHeight)) { currentHeight = 0; } var currentWidth = boundingRect.width; if (currentWidth <= 0 || isNaN(currentWidth)) { currentWidth = 0; } if (currentHeight >= height) { height = currentHeight; } x += currentWidth + offsetX; } return { nodes: resultNodeSet, links: resultLinkSet }; }, moveToOffset: function (component, p) { var i, j, bounds = component.bounds, deltax = p.x - bounds.x, deltay = p.y - bounds.y; for (i = 0; i < component.nodes.length; i++) { var node = component.nodes[i]; var nodeBounds = node.bounds(); if (nodeBounds.width === 0 && nodeBounds.height === 0 && nodeBounds.x === 0 && nodeBounds.y === 0) { nodeBounds = new Rect(0, 0, 0, 0); } nodeBounds.x += deltax; nodeBounds.y += deltay; node.bounds(nodeBounds); } for (i = 0; i < component.links.length; i++) { var link = component.links[i]; if (link.points) { var newpoints = []; var points = link.points; for (j = 0; j < points.length; j++) { var pt = points[j]; pt.x += deltax; pt.y += deltay; newpoints.push(pt); } link.points = newpoints; } } this.currentHorizontalOffset += bounds.width + this.options.grid.offsetX; return new Point(deltax, deltay); }, transferOptions: function (options) { // Size options lead to stackoverflow and need special handling this.options = kendo.deepExtend({}, this.defaultOptions); if (Utils.isUndefined(options)) { return; } this.options = kendo.deepExtend(this.options, options || {}); } }); /** * The data bucket a hypertree holds in its nodes. * * @type {*} */ /* var ContainerGraph = kendo.Class.extend({ init: function (diagram) { this.diagram = diagram; this.graph = new Graph(diagram); this.container = null; this.containerNode = null; } });*/ /** * Adapter between the diagram control and the graph representation. It converts shape and connections to nodes and edges taking into the containers and their collapsef state, * the visibility of items and more. If the layoutContainerChildren is true a hypertree is constructed which holds the hierarchy of containers and many conditions are analyzed * to investigate how the effective graph structure looks like and how the layout has to be performed. * @type {*} */ var DiagramToHyperTreeAdapter = kendo.Class.extend({ init: function (diagram) { /** * The mapping to/from the original nodes. * @type {Dictionary} */ this.nodeMap = new Dictionary(); /** * Gets the mapping of a shape to a container in case the shape sits in a collapsed container. * @type {Dictionary} */ this.shapeMap = new Dictionary(); /** * The nodes being mapped. * @type {Dictionary} */ this.nodes = []; /** * The connections being mapped. * @type {Dictionary} */ this.edges = []; // the mapping from an edge to all the connections it represents, this can be both because of multiple connections between // two shapes or because a container holds multiple connections to another shape or container. this.edgeMap = new Dictionary(); /** * The resulting set of Nodes when the analysis has finished. * @type {Array} */ this.finalNodes = []; /** * The resulting set of Links when the analysis has finished. * @type {Array} */ this.finalLinks = []; /** * The items being omitted because of multigraph edges. * @type {Array} */ this.ignoredConnections = []; /** * The items being omitted because of containers, visibility and other factors. * @type {Array} */ this.ignoredShapes = []; /** * The map from a node to the partition/hypernode in which it sits. This hyperMap is null if 'options.layoutContainerChildren' is false. * @type {Dictionary} */ this.hyperMap = new Dictionary(); /** * The hypertree contains the hierarchy defined by the containers. * It's in essence a Graph of Graphs with a tree structure defined by the hierarchy of containers. * @type {HyperTree} */ this.hyperTree = new Graph(); /** * The resulting graph after conversion. Note that this does not supply the information contained in the * ignored connection and shape collections. * @type {null} */ this.finalGraph = null; this.diagram = diagram; }, /** * The hyperTree is used when the 'options.layoutContainerChildren' is true. It contains the hierarchy of containers whereby each node is a ContainerGraph. * This type of node has a Container reference to the container which holds the Graph items. There are three possible situations during the conversion process: * - Ignore the containers: the container are non-existent and only normal shapes are mapped. If a shape has a connection to a container it will be ignored as well * since there is no node mapped for the container. * - Do not ignore the containers and leave the content of the containers untouched: the top-level elements are being mapped and the children within a container are not altered. * - Do not ignore the containers and organize the content of the containers as well: the hypertree is constructed and there is a partitioning of all nodes and connections into the hypertree. * The only reason a connection or node is not being mapped might be due to the visibility, which includes the visibility change through a collapsed parent container. * @param options */ convert: function (options) { if (Utils.isUndefined(this.diagram)) { throw "No diagram to convert."; } this.options = kendo.deepExtend({ ignoreInvisible: true, ignoreContainers: true, layoutContainerChildren: false }, options || {} ); this.clear(); // create the nodes which participate effectively in the graph analysis this._renormalizeShapes(); // recreate the incoming and outgoing collections of each and every node this._renormalizeConnections(); // export the resulting graph this.finalNodes = new Dictionary(this.nodes); this.finalLinks = new Dictionary(this.edges); this.finalGraph = new Graph(); this.finalNodes.forEach(function (n) { this.finalGraph.addNode(n); }, this); this.finalLinks.forEach(function (l) { this.finalGraph.addExistingLink(l); }, this); return this.finalGraph; }, /** * Maps the specified connection to an edge of the graph deduced from the given diagram. * @param connection * @returns {*} */ mapConnection: function (connection) { return this.edgeMap.get(connection.id); }, /** * Maps the specified shape to a node of the graph deduced from the given diagram. * @param shape * @returns {*} */ mapShape: function (shape) { return this.nodeMap.get(shape.id); }, /** * Gets the edge, if any, between the given nodes. * @param a * @param b */ getEdge: function (a, b) { return Utils.first(a.links, function (link) { return link.getComplement(a) === b; }); }, /** * Clears all the collections used by the conversion process. */ clear: function () { this.finalGraph = null; this.hyperTree = (!this.options.ignoreContainers && this.options.layoutContainerChildren) ? new HyperTree() : null; this.hyperMap = (!this.options.ignoreContainers && this.options.layoutContainerChildren) ? new Dictionary() : null; this.nodeMap = new Dictionary(); this.shapeMap = new Dictionary(); this.nodes = []; this.edges = []; this.edgeMap = new Dictionary(); this.ignoredConnections = []; this.ignoredShapes = []; this.finalNodes = []; this.finalLinks = []; }, /** * The path from a given ContainerGraph to the root (container). * @param containerGraph * @returns {Array} */ listToRoot: function (containerGraph) { var list = []; var s = containerGraph.container; if (!s) { return list; } list.push(s); while (s.parentContainer) { s = s.parentContainer; list.push(s); } list.reverse(); return list; }, firstNonIgnorableContainer: function (shape) { if (shape.isContainer && !this._isIgnorableItem(shape)) { return shape; } return !shape.parentContainer ? null : this.firstNonIgnorableContainer(shape.parentContainer); }, isContainerConnection: function (a, b) { if (a.isContainer && this.isDescendantOf(a, b)) { return true; } return b.isContainer && this.isDescendantOf(b, a); }, /** * Returns true if the given shape is a direct child or a nested container child of the given container. * If the given container and shape are the same this will return false since a shape cannot be its own child. * @param scope * @param a * @returns {boolean} */ isDescendantOf: function (scope, a) { if (!scope.isContainer) { throw "Expecting a container."; } if (scope === a) { return false; } if (contains(scope.children, a)) { return true; } var containers = []; for (var i = 0, len = scope.children.length; i < len; i++) { var c = scope.children[i]; if (c.isContainer && this.isDescendantOf(c, a)) { containers.push(c); } } return containers.length > 0; }, isIgnorableItem: function (shape) { if (this.options.ignoreInvisible) { if (shape.isCollapsed && this._isVisible(shape)) { return false; } if (!shape.isCollapsed && this._isVisible(shape)) { return false; } return true; } else { return shape.isCollapsed && !this._isTop(shape); } }, /** * Determines whether the shape is or needs to be mapped to another shape. This occurs essentially when the shape sits in * a collapsed container hierarchy and an external connection needs a node endpoint. This node then corresponds to the mapped shape and is * necessarily a container in the parent hierarchy of the shape. * @param shape */ isShapeMapped: function (shape) { return shape.isCollapsed && !this._isVisible(shape) && !this._isTop(shape); }, leastCommonAncestor: function (a, b) { if (!a) { throw "Parameter should not be null."; } if (!b) { throw "Parameter should not be null."; } if (!this.hyperTree) { throw "No hypertree available."; } var al = this.listToRoot(a); var bl = this.listToRoot(b); var found = null; if (Utils.isEmpty(al) || Utils.isEmpty(bl)) { return this.hyperTree.root.data; } var xa = al[0]; var xb = bl[0]; var i = 0; while (xa === xb) { found = al[i]; i++; if (i >= al.length || i >= bl.length) { break; } xa = al[i]; xb = bl[i]; } if (!found) { return this.hyperTree.root.data; } else { return grep(this.hyperTree.nodes, function (n) { return n.data.container === found; }); } }, /** * Determines whether the specified item is a top-level shape or container. * @param item * @returns {boolean} * @private */ _isTop: function (item) { return !item.parentContainer; }, /** * Determines iteratively (by walking up the container stack) whether the specified shape is visible. * This does NOT tell whether the item is not visible due to an explicit Visibility change or due to a collapse state. * @param shape * @returns {*} * @private */ _isVisible: function (shape) { if (!shape.visible()) { return false; } return !shape.parentContainer ? shape.visible() : this._isVisible(shape.parentContainer); }, _isCollapsed: function (shape) { if (shape.isContainer && shape.isCollapsed) { return true; } return shape.parentContainer && this._isCollapsed(shape.parentContainer); }, /** * First part of the graph creation; analyzing the shapes and containers and deciding whether they should be mapped to a Node. * @private */ _renormalizeShapes: function () { // add the nodes, the adjacency structure will be reconstructed later on if (this.options.ignoreContainers) { for (var i = 0, len = this.diagram.shapes.length; i < len; i++) { var shape = this.diagram.shapes[i]; // if not visible (and ignoring the invisible ones) or a container we skip if ((this.options.ignoreInvisible && !this._isVisible(shape)) || shape.isContainer) { this.ignoredShapes.push(shape); continue; } var node = new Node(shape.id, shape); node.isVirtual = false; // the mapping will always contain singletons and the hyperTree will be null this.nodeMap.add(shape.id, node); this.nodes.push(node); } } else { throw "Containers are not supported yet, but stay tuned."; } }, /** * Second part of the graph creation; analyzing the connections and deciding whether they should be mapped to an edge. * @private */ _renormalizeConnections: function () { if (this.diagram.connections.length === 0) { return; } for (var i = 0, len = this.diagram.connections.length; i < len; i++) { var conn = this.diagram.connections[i]; if (this.isIgnorableItem(conn)) { this.ignoredConnections.push(conn); continue; } var source = !conn.sourceConnector ? null : conn.sourceConnector.shape; var sink = !conn.targetConnector ? null : conn.targetConnector.shape; // no layout for floating connections if (!source || !sink) { this.ignoredConnections.push(conn); continue; } if (contains(this.ignoredShapes, source) && !this.shapeMap.containsKey(source)) { this.ignoredConnections.push(conn); continue; } if (contains(this.ignoredShapes, sink) && !this.shapeMap.containsKey(sink)) { this.ignoredConnections.push(conn); continue; } // if the endpoint sits in a collapsed container we need the container rather than the shape itself if (this.shapeMap.containsKey(source)) { source = this.shapeMap[source]; } if (this.shapeMap.containsKey(sink)) { sink = this.shapeMap[sink]; } var sourceNode = this.mapShape(source); var sinkNode = this.mapShape(sink); if ((sourceNode === sinkNode) || this.areConnectedAlready(sourceNode, sinkNode)) { this.ignoredConnections.push(conn); continue; } if (sourceNode === null || sinkNode === null) { throw "A shape was not mapped to a node."; } if (this.options.ignoreContainers) { // much like a floating connection here since at least one end is attached to a container if (sourceNode.isVirtual || sinkNode.isVirtual) { this.ignoredConnections.push(conn); continue; } var newEdge = new Link(sourceNode, sinkNode, conn.id, conn); this.edgeMap.add(conn.id, newEdge); this.edges.push(newEdge); } else { throw "Containers are not supported yet, but stay tuned."; } } }, areConnectedAlready: function (n, m) { return Utils.any(this.edges, function (l) { return l.source === n && l.target === m || l.source === m && l.target === n; }); } /** * Depth-first traversal of the given container. * @param container * @param action * @param includeStart * @private */ /* _visitContainer: function (container, action, includeStart) { *//*if (container == null) throw new ArgumentNullException("container"); if (action == null) throw new ArgumentNullException("action"); if (includeStart) action(container); if (container.children.isEmpty()) return; foreach( var item in container.children.OfType < IShape > () ) { var childContainer = item as IContainerShape; if (childContainer != null) this.VisitContainer(childContainer, action); else action(item); }*//* }*/ }); /** * The classic spring-embedder (aka force-directed, Fruchterman-Rheingold, barycentric) algorithm. * http://en.wikipedia.org/wiki/Force-directed_graph_drawing * - Chapter 12 of Tamassia et al. "Handbook of graph drawing and visualization". * - Kobourov on preprint arXiv; http://arxiv.org/pdf/1201.3011.pdf * - Fruchterman and Rheingold in SOFTWARE-PRACTICE AND EXPERIENCE, VOL. 21(1 1), 1129-1164 (NOVEMBER 1991) * @type {*} */ var SpringLayout = LayoutBase.extend({ init: function (diagram) { var that = this; LayoutBase.fn.init.call(that); if (Utils.isUndefined(diagram)) { throw "Diagram is not specified."; } this.diagram = diagram; }, layout: function (options) { this.transferOptions(options); var adapter = new DiagramToHyperTreeAdapter(this.diagram); var graph = adapter.convert(options); if (graph.isEmpty()) { return; } // split into connected components var components = graph.getConnectedComponents(); if (Utils.isEmpty(components)) { return; } for (var i = 0; i < components.length; i++) { var component = components[i]; this.layoutGraph(component, options); } var finalNodeSet = this.gridLayoutComponents(components); return new diagram.LayoutState(this.diagram, finalNodeSet); }, layoutGraph: function (graph, options) { if (Utils.isDefined(options)) { this.transferOptions(options); } this.graph = graph; var initialTemperature = this.options.nodeDistance * 9; this.temperature = initialTemperature; var guessBounds = this._expectedBounds(); this.width = guessBounds.width; this.height = guessBounds.height; for (var step = 0; step < this.options.iterations; step++) { this.refineStage = step >= this.options.iterations * 5 / 6; this.tick(); // exponential cooldown this.temperature = this.refineStage ? initialTemperature / 30 : initialTemperature * (1 - step / (2 * this.options.iterations )); } }, /** * Single iteration of the simulation. */ tick: function () { var i; // collect the repulsive forces on each node for (i = 0; i < this.graph.nodes.length; i++) { this._repulsion(this.graph.nodes[i]); } // collect the attractive forces on each node for (i = 0; i < this.graph.links.length; i++) { this._attraction(this.graph.links[i]); } // update the positions for (i = 0; i < this.graph.nodes.length; i++) { var node = this.graph.nodes[i]; var offset = Math.sqrt(node.dx * node.dx + node.dy * node.dy); if (offset === 0) { return; } node.x += Math.min(offset, this.temperature) * node.dx / offset; node.y += Math.min(offset, this.temperature) * node.dy / offset; if (this.options.limitToView) { node.x = Math.min(this.width, Math.max(node.width / 2, node.x)); node.y = Math.min(this.height, Math.max(node.height / 2, node.y)); } } }, /** * Shakes the node away from its current position to escape the deadlock. * @param node A Node. * @private */ _shake: function (node) { // just a simple polar neighborhood var rho = Math.random() * this.options.nodeDistance / 4; var alpha = Math.random() * 2 * Math.PI; node.x += rho * Math.cos(alpha); node.y -= rho * Math.sin(alpha); }, /** * The typical Coulomb-Newton force law F=k/r^2 * @remark This only works in dimensions less than three. * @param d * @param n A Node. * @param m Another Node. * @returns {number} * @private */ _InverseSquareForce: function (d, n, m) { var force; if (!this.refineStage) { force = Math.pow(d, 2) / Math.pow(this.options.nodeDistance, 2); } else { var deltax = n.x - m.x; var deltay = n.y - m.y; var wn = n.width / 2; var hn = n.height / 2; var wm = m.width / 2; var hm = m.height / 2; force = (Math.pow(deltax, 2) / Math.pow(wn + wm + this.options.nodeDistance, 2)) + (Math.pow(deltay, 2) / Math.pow(hn + hm + this.options.nodeDistance, 2)); } return force * 4 / 3; }, /** * The typical Hooke force law F=kr^2 * @param d * @param n * @param m * @returns {number} * @private */ _SquareForce: function (d, n, m) { return 1 / this._InverseSquareForce(d, n, m); }, _repulsion: function (n) { n.dx = 0; n.dy = 0; Utils.forEach(this.graph.nodes, function (m) { if (m === n) { return; } while (n.x === m.x && n.y === m.y) { this._shake(m); } var vx = n.x - m.x; var vy = n.y - m.y; var distance = Math.sqrt(vx * vx + vy * vy); var r = this._SquareForce(distance, n, m) * 2; n.dx += (vx / distance) * r; n.dy += (vy / distance) * r; }, this); }, _attraction: function (link) { var t = link.target; var s = link.source; if (s === t) { // loops induce endless shakes return; } while (s.x === t.x && s.y === t.y) { this._shake(t); } var vx = s.x - t.x; var vy = s.y - t.y; var distance = Math.sqrt(vx * vx + vy * vy); var a = this._InverseSquareForce(distance, s, t) * 5; var dx = (vx / distance) * a; var dy = (vy / distance) * a; t.dx += dx; t.dy += dy; s.dx -= dx; s.dy -= dy; }, /** * Calculates the expected bounds after layout. * @returns {*} * @private */ _expectedBounds: function () { var size, N = this.graph.nodes.length, /*golden ration optimal?*/ ratio = 1.5, multiplier = 4; if (N === 0) { return size; } size = Utils.fold(this.graph.nodes, function (s, node) { var area = node.width * node.height; if (area > 0) { s += Math.sqrt(area); return s; } return 0; }, 0, this); var av = size / N; var squareSize = av * Math.ceil(Math.sqrt(N)); var width = squareSize * Math.sqrt(ratio); var height = squareSize / Math.sqrt(ratio); return { width: width * multiplier, height: height * multiplier }; } }); var TreeLayoutProcessor = kendo.Class.extend({ init: function (options) { this.center = null; this.options = options; }, layout: function (treeGraph, root) { this.graph = treeGraph; if (!this.graph.nodes || this.graph.nodes.length === 0) { return; } if (!contains(this.graph.nodes, root)) { throw "The given root is not in the graph."; } this.center = root; this.graph.cacheRelationships(); /* var nonull = this.graph.nodes.where(function (n) { return n.associatedShape != null; });*/ // transfer the rects /*nonull.forEach(function (n) { n.Location = n.associatedShape.Position; n.NodeSize = n.associatedShape.ActualBounds.ToSize(); } );*/ // caching the children /* nonull.forEach(function (n) { n.children = n.getChildren(); });*/ this.layoutSwitch(); // apply the layout to the actual visuals // nonull.ForEach(n => n.associatedShape.Position = n.Location); }, layoutLeft: function (left) { this.setChildrenDirection(this.center, "Left", false); this.setChildrenLayout(this.center, "Default", false); var h = 0, w = 0, y, i, node; for (i = 0; i < left.length; i++) { node = left[i]; node.TreeDirection = "Left"; var s = this.measure(node, Size.Empty); w = Math.max(w, s.Width); h += s.height + this.options.verticalSeparation; } h -= this.options.verticalSeparation; var x = this.center.x - this.options.horizontalSeparation; y = this.center.y + ((this.center.height - h) / 2); for (i = 0; i < left.length; i++) { node = left[i]; var p = new Point(x - node.Size.width, y); this.arrange(node, p); y += node.Size.height + this.options.verticalSeparation; } }, layoutRight: function (right) { this.setChildrenDirection(this.center, "Right", false); this.setChildrenLayout(this.center, "Default", false); var h = 0, w = 0, y, i, node; for (i = 0; i < right.length; i++) { node = right[i]; node.TreeDirection = "Right"; var s = this.measure(node, Size.Empty); w = Math.max(w, s.Width); h += s.height + this.options.verticalSeparation; } h -= this.options.verticalSeparation; var x = this.center.x + this.options.horizontalSeparation + this.center.width; y = this.center.y + ((this.center.height - h) / 2); for (i = 0; i < right.length; i++) { node = right[i]; var p = new Point(x, y); this.arrange(node, p); y += node.Size.height + this.options.verticalSeparation; } }, layoutUp: function (up) { this.setChildrenDirection(this.center, "Up", false); this.setChildrenLayout(this.center, "Default", false); var w = 0, y, node, i; for (i = 0; i < up.length; i++) { node = up[i]; node.TreeDirection = "Up"; var s = this.measure(node, Size.Empty); w += s.width + this.options.horizontalSeparation; } w -= this.options.horizontalSeparation; var x = this.center.x + (this.center.width / 2) - (w / 2); // y = this.center.y -verticalSeparation -this.center.height/2 - h; for (i = 0; i < up.length; i++) { node = up[i]; y = this.center.y - this.options.verticalSeparation - node.Size.height; var p = new Point(x, y); this.arrange(node, p); x += node.Size.width + this.options.horizontalSeparation; } }, layoutDown: function (down) { var node, i; this.setChildrenDirection(this.center, "Down", false); this.setChildrenLayout(this.center, "Default", false); var w = 0, y; for (i = 0; i < down.length; i++) { node = down[i]; node.treeDirection = "Down"; var s = this.measure(node, Size.Empty); w += s.width + this.options.horizontalSeparation; } w -= this.options.horizontalSeparation; var x = this.center.x + (this.center.width / 2) - (w / 2); y = this.center.y + this.options.verticalSeparation + this.center.height; for (i = 0; i < down.length; i++) { node = down[i]; var p = new Point(x, y); this.arrange(node, p); x += node.Size.width + this.options.horizontalSeparation; } }, layoutRadialTree: function () { // var rmax = children.Aggregate(0D, (current, node) => Math.max(node.SectorAngle, current)); this.setChildrenDirection(this.center, "Radial", false); this.setChildrenLayout(this.center, "Default", false); this.previousRoot = null; var startAngle = this.options.startRadialAngle * DEG_TO_RAD; var endAngle = this.options.endRadialAngle * DEG_TO_RAD; if (endAngle <= startAngle) { throw "Final angle should not be less than the start angle."; } this.maxDepth = 0; this.origin = new Point(this.center.x, this.center.y); this.calculateAngularWidth(this.center, 0); // perform the layout if (this.maxDepth > 0) { this.radialLayout(this.center, this.options.radialFirstLevelSeparation, startAngle, endAngle); } // update properties of the root node this.center.Angle = endAngle - startAngle; }, tipOverTree: function (down, startFromLevel) { if (Utils.isUndefined(startFromLevel)) { startFromLevel = 0; } this.setChildrenDirection(this.center, "Down", false); this.setChildrenLayout(this.center, "Default", false); this.setChildrenLayout(this.center, "Underneath", false, startFromLevel); var w = 0, y, node, i; for (i = 0; i < down.length; i++) { node = down[i]; // if (node.IsSpecial) continue; node.TreeDirection = "Down"; var s = this.measure(node, Size.Empty); w += s.width + this.options.horizontalSeparation; } w -= this.options.horizontalSeparation; // putting the root in the center with respect to the whole diagram is not a nice result, let's put it with respect to the first level only w -= down[down.length - 1].width; w += down[down.length - 1].associatedShape.bounds().width; var x = this.center.x + (this.center.width / 2) - (w / 2); y = this.center.y + this.options.verticalSeparation + this.center.height; for (i = 0; i < down.length; i++) { node = down[i]; // if (node.IsSpecial) continue; var p = new Point(x, y); this.arrange(node, p); x += node.Size.width + this.options.horizontalSeparation; } /*//let's place the special node, assuming there is only one if (down.Count(n => n.IsSpecial) > 0) { var special = (from n in down where n.IsSpecial select n).First(); if (special.Children.Count > 0) throw new DiagramException("The 'special' element should not have children."); special.Data.Location = new Point(Center.Data.Location.X + Center.AssociatedShape.BoundingRectangle.Width + this.options.HorizontalSeparation, Center.Data.Location.Y); }*/ }, calculateAngularWidth: function (n, d) { if (d > this.maxDepth) { this.maxDepth = d; } var aw = 0, w = 1000, h = 1000, diameter = d === 0 ? 0 : Math.sqrt((w * w) + (h * h)) / d; if (n.children.length > 0) { // eventually with n.IsExpanded for (var i = 0, len = n.children.length; i < len; i++) { var child = n.children[i]; aw += this.calculateAngularWidth(child, d + 1); } aw = Math.max(diameter, aw); } else { aw = diameter; } n.sectorAngle = aw; return aw; }, sortChildren: function (n) { var basevalue = 0, i; // update basevalue angle for node ordering if (n.parents.length > 1) { throw "Node is not part of a tree."; } var p = n.parents[0]; if (p) { var pl = new Point(p.x, p.y); var nl = new Point(n.x, n.y); basevalue = this.normalizeAngle(Math.atan2(pl.y - nl.y, pl.x - nl.x)); } var count = n.children.length; if (count === 0) { return null; } var angle = []; var idx = []; for (i = 0; i < count; ++i) { var c = n.children[i]; var l = new Point(c.x, c.y); idx[i] = i; angle[i] = this.normalizeAngle(-basevalue + Math.atan2(l.y - l.y, l.x - l.x)); } Utils.bisort(angle, idx); var col = []; // list of nodes var children = n.children; for (i = 0; i < count; ++i) { col.push(children[idx[i]]); } return col; }, normalizeAngle: function (angle) { while (angle > Math.PI * 2) { angle -= 2 * Math.PI; } while (angle < 0) { angle += Math.PI * 2; } return angle; }, radialLayout: function (node, radius, startAngle, endAngle) { var deltaTheta = endAngle - startAngle; var deltaThetaHalf = deltaTheta / 2.0; var parentSector = node.sectorAngle; var fraction = 0; var sorted = this.sortChildren(node); for (var i = 0, len = sorted.length; i < len; i++) { var childNode = sorted[i]; var cp = childNode; var childAngleFraction = cp.sectorAngle / parentSector; if (childNode.children.length > 0) { this.radialLayout(childNode, radius + this.options.radialSeparation, startAngle + (fraction * deltaTheta), startAngle + ((fraction + childAngleFraction) * deltaTheta)); } this.setPolarLocation(childNode, radius, startAngle + (fraction * deltaTheta) + (childAngleFraction * deltaThetaHalf)); cp.angle = childAngleFraction * deltaTheta; fraction += childAngleFraction; } }, setPolarLocation: function (node, radius, angle) { node.x = this.origin.x +