UNPKG

gojs

Version:

Interactive diagrams, charts, and graphs, such as trees, flowcharts, orgcharts, UML, BPMN, or business diagrams

272 lines (245 loc) 8.71 kB
"use strict"; /* * Copyright (C) 1998-2017 by Northwoods Software Corporation. All Rights Reserved. */ import * as go from "../release/go"; /** * Given a root Node this arranges connected nodes in concentric rings, * layered by the minimum link distance from the root. */ export class RadialLayout extends go.Layout { private _root: go.Node = null; private _layerThickness: number = 100; // how thick each ring should be private _maxLayers: number = Infinity; /** * Copies properties to a cloned Layout. */ protected cloneProtected(copy: RadialLayout) { super.cloneProtected(copy); // don't copy .root copy._layerThickness = this._layerThickness; copy._maxLayers = this._maxLayers; } /* * The Node to act as the root or central node of the radial layout. */ get root(): go.Node { return this._root; } set root(value: go.Node) { if (this._root !== value) { this._root = value; this.invalidateLayout(); } } /* * The thickness of each ring representing a layer. */ get layerThickness(): number { return this._layerThickness; } set layerThickness(value: number) { if (this._layerThickness !== value) { this._layerThickness = value; this.invalidateLayout(); } } /* * The maximum number of layers to be shown, in addition to the root node at layer zero. * The default value is Infinity. */ get maxLayers(): number { return this._maxLayers; } set maxLayers(value: number) { if (this._maxLayers !== value) { this._maxLayers = value; this.invalidateLayout(); } } /** * Use a LayoutNetwork that always creates RadialVertexes. */ public createNetwork() { var net = new go.LayoutNetwork(); net.createVertex = () => new RadialVertex(); return net; } /** */ public doLayout(coll: go.Diagram | go.Group | go.Iterable<go.Part>) { if (this.network === null) { this.network = this.makeNetwork(coll); } if (this.root === null) { // If no root supplied, choose one without any incoming edges var it = this.network.vertexes.iterator; while (it.next()) { var v = it.value; if (v.node !== null && v.sourceEdges.count === 0) { this.root = v.node; break; } } } if (this.root === null) { // If could not find any default root, choose a random one this.root = this.network.vertexes.first().node; } if (this.root === null) return; // nothing to do var rootvert = this.network.findVertex(this.root) as RadialVertex; if (rootvert === null) throw new Error("RadialLayout.root must be a Node in the LayoutNetwork that the RadialLayout is operating on") this.arrangementOrigin = this.initialOrigin(this.arrangementOrigin); this.findDistances(rootvert); // sort all results into Arrays of RadialVertexes with the same distance var verts = []; var maxlayer = 0; var it = this.network.vertexes.iterator; while (it.next()) { var vv = it.value as RadialVertex; vv.laid = false; var layer = vv.distance; if (layer === Infinity) continue; // Infinity used as init value (set in findDistances()) if (layer > maxlayer) maxlayer = layer; var layerverts: Array<go.LayoutVertex> = verts[layer]; if (layerverts === undefined) { layerverts = []; verts[layer] = layerverts; } layerverts.push(vv); } // now recursively position nodes (using radlay1()), starting with the root rootvert.centerX = this.arrangementOrigin.x; rootvert.centerY = this.arrangementOrigin.y; this.radlay1(rootvert, 1, 0, 360); // Update the "physical" positions of the nodes and links. this.updateParts(); this.network = null; } /** * recursively position vertexes in a radial layout */ private radlay1(vert: RadialVertex, layer: number, angle: number, sweep: number) { if (layer > this.maxLayers) return; // no need to position nodes outside of maxLayers var verts: Array<RadialVertex> = []; // array of all RadialVertexes connected to 'vert' in layer 'layer' vert.vertexes.each(function (v: RadialVertex) { if (v.laid) return; if (v.distance === layer) verts.push(v); }); var found = verts.length; if (found === 0) return; var radius = layer * this.layerThickness; var separator = sweep / found; // distance between nodes in their sweep portion var start = angle - sweep / 2 + separator / 2; // for each vertex in this layer, place it in its correct layer and position for (var i = 0; i < found; i++) { var v = verts[i]; var a = start + i * separator; // the angle to rotate the node to if (a < 0) a += 360; else if (a > 360) a -= 360; // the point to place the node at -- this corresponds with the layer the node is in // all nodes in the same layer are placed at a constant point, then rotated accordingly var p = new go.Point(radius, 0); p.rotate(a); v.centerX = p.x + this.arrangementOrigin.x; v.centerY = p.y + this.arrangementOrigin.y; v.laid = true; v.angle = a; v.sweep = separator; v.radius = radius; // keep going for all layers this.radlay1(v, layer + 1, a, sweep / found); } } /** * Update RadialVertex.distance for every vertex. */ private findDistances(source: RadialVertex) { var diagram = this.diagram; // keep track of distances from the source node this.network.vertexes.each(function (v: RadialVertex) { v.distance = Infinity; }); // the source node starts with distance 0 source.distance = 0; // keep track of nodes for we have set a non-Infinity distance, // but which we have not yet finished examining var seen = new go.Set(RadialVertex); seen.add(source); // local function for finding a vertex with the smallest distance in a given collection function leastVertex(coll: go.Set<RadialVertex>) { var bestdist = Infinity; var bestvert = null; var it = coll.iterator; while (it.next()) { var v = it.value; var dist = v.distance; if (dist < bestdist) { bestdist = dist; bestvert = v; } } return bestvert; } // keep track of vertexes we have finished examining; // this avoids unnecessary traversals and helps keep the SEEN collection small var finished = new go.Set(RadialVertex); while (seen.count > 0) { // look at the unfinished vertex with the shortest distance so far var least = leastVertex(seen as go.Set<RadialVertex>); var leastdist = least.distance; // by the end of this loop we will have finished examining this LEAST vertex seen.remove(least); finished.add(least); // look at all edges connected with this vertex least.edges.each(function (e) { var neighbor = e.getOtherVertex(least); // skip vertexes that we have finished if (finished.contains(<any>neighbor)) return; var neighbordist = (<any>neighbor).distance; // assume "distance" along a link is unitary, but could be any non-negative number. var dist = leastdist + 1; if (dist < neighbordist) { // if haven't seen that vertex before, add it to the SEEN collection if (neighbordist == Infinity) { seen.add(<any>neighbor); } // record the new best distance so far to that node (<any>neighbor).distance = dist; } }); } } /** * This override positions each Node and also calls {@link #rotateNode}. */ commitLayout() { super.commitLayout(); var it = this.network.vertexes.iterator; while (it.next()) { var v = it.value as RadialVertex; var n = v.node; if (n !== null) { n.visible = (v.distance <= this.maxLayers); this.rotateNode(n, v.angle, v.sweep, v.radius); } } this.commitLayers(); } /** * Override this method in order to modify each node as it is laid out. * By default this method does nothing. */ rotateNode(node: go.Node, angle: number, sweep: number, radius: number) { } /** * Override this method in order to create background circles indicating the layers of the radial layout. * By default this method does nothing. */ commitLayers() { } } // end RadialLayout /** * @ignore * @constructor * @extends LayoutVertex * @class */ class RadialVertex extends go.LayoutVertex { distance: number = Infinity; // number of layers from the root, non-negative integers laid: boolean = false; // used internally to keep track angle: number = 0; // the direction at which the node is placed relative to the root node sweep: number = 0; // the angle subtended by the vertex radius: number = 0; // the inner radius of the layer containing this vertex }