gojs
Version:
Interactive diagrams, charts, and graphs, such as trees, flowcharts, orgcharts, UML, BPMN, or business diagrams
356 lines (330 loc) • 13.6 kB
HTML
<html>
<head>
<title>Radial Layout</title>
<meta name="description" content="Radial layout of an arbitrary graph given a start node; selecting a node re-lays out using it as a new root node." />
<!-- Copyright 1998-2016 by Northwoods Software Corporation. -->
<meta charset="UTF-8">
<script src="go.js"></script>
<link href="../assets/css/goSamples.css" rel="stylesheet" type="text/css" /> <!-- you don't need to use this -->
<script src="goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
<script id="code">
var showCircles = true; // show a circle behind the nodes in each layer
var rotateText = true; // whether to rotate the label with the angle of the node
var maxLayers = 2; // how many concentric layers to show
var layerThickness = 100; // how thick each ring should be
function init() {
if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this
var $ = go.GraphObject.make; // for conciseness in defining templates
myDiagram =
$(go.Diagram, "myDiagramDiv", // must be the ID or reference to div
{
initialAutoScale: go.Diagram.Uniform,
initialContentAlignment: go.Spot.Center,
padding: 10,
isReadOnly: true,
maxSelectionCount: 1,
"animationManager.isEnabled": false
});
// shows when hovering over a node
var commonToolTip =
$(go.Adornment, "Auto",
{ isShadowed: true },
$(go.Shape, { fill: "#FFFFCC" }),
$(go.Panel, "Vertical",
{ margin: 3 },
$(go.TextBlock, // bound to node data
{margin: 4, font: "bold 12pt sans-serif" },
new go.Binding("text")),
$(go.TextBlock, // bound to node data
new go.Binding("text", "color", function(c) { return "Color: " + c; })),
$(go.TextBlock, // bound to Adornment because of call to Binding.ofObject
new go.Binding("text", "", function(ad) { return "Connections: " + ad.adornedPart.linksConnected.count; }).ofObject())
) // end Vertical Panel
); // end Adornment
// define the Node template
myDiagram.nodeTemplate =
$(go.Node, "Spot",
{
locationSpot: go.Spot.Center,
locationObjectName: "SHAPE", // Node.location is the center of the Shape
selectionAdorned: false,
selectionChanged: nodeSelectionChanged,
toolTip: commonToolTip
},
$(go.Shape, "Circle",
{
name: "SHAPE",
fill: "lightgray", // default value, but also data-bound
stroke: "transparent",
strokeWidth: 2,
desiredSize: new go.Size(20, 20),
portId: "" // so links will go to the shape, not the whole node
},
new go.Binding("fill", "color")),
$(go.TextBlock,
{
name: "TEXTBLOCK",
alignment: go.Spot.Right,
alignmentFocus: go.Spot.Left
},
new go.Binding("text"))
);
// this is the root node, at the center of the circular layers
myDiagram.nodeTemplateMap.add("Root",
$(go.Node, "Auto",
{
locationSpot: go.Spot.Center,
selectionAdorned: false,
selectionChanged: nodeSelectionChanged,
toolTip: commonToolTip
},
$(go.Shape, "Circle",
{ fill: "white" }),
$(go.TextBlock,
{ font: "bold 14pt sans-serif", margin: 10 },
new go.Binding("text"))
));
// define the Link template
myDiagram.linkTemplate =
$(go.Link,
{
routing: go.Link.Normal,
curve: go.Link.Bezier,
selectionAdorned: false,
layerName: "Background"
},
$(go.Shape,
{ stroke: "black", // default value, but is data-bound
strokeWidth: 1 },
new go.Binding("stroke", "color"))
);
generateGraph();
}
function generateGraph() {
var names = [
"Joshua", "Daniel", "Robert", "Noah", "Anthony",
"Elizabeth", "Addison", "Alexis", "Ella", "Samantha",
"Joseph", "Scott", "James", "Ryan", "Benjamin",
"Walter", "Gabriel", "Christian", "Nathan", "Simon",
"Isabella", "Emma", "Olivia", "Sophia", "Ava",
"Emily", "Madison", "Tina", "Elena", "Mia",
"Jacob", "Ethan", "Michael", "Alexander", "William",
"Natalie", "Grace", "Lily", "Alyssa", "Ashley",
"Sarah", "Taylor", "Hannah", "Brianna", "Hailey",
"Christopher", "Aiden", "Matthew", "David", "Andrew",
"Kaylee", "Juliana", "Leah", "Anna", "Allison",
"John", "Samuel", "Tyler", "Dylan", "Jonathan"
];
var nodeDataArray = [];
for (var i = 0; i < names.length; i++) {
nodeDataArray.push({ key: i, text: names[i], color: go.Brush.randomColor(128, 240) });
}
var linkDataArray = [];
var num = nodeDataArray.length;
for (var i = 0; i < num * 2; i++) {
var a = Math.floor(Math.random() * num);
var b = Math.floor(Math.random() * num / 4) + 1;
linkDataArray.push({ from: a, to: (a + b) % num, color: go.Brush.randomColor(0, 127) });
}
myDiagram.model = new go.GraphLinksModel(nodeDataArray, linkDataArray);
var someone = nodeDataArray[Math.floor(Math.random() * nodeDataArray.length)];
var somenode = myDiagram.findNodeForData(someone);
myDiagram.select(somenode);
}
// called when "Set Max Layers" button is clicked
function adjustMaxLayers() {
var newMaxLayers = document.getElementById("maxLayersChanger").value;
function IsNumeric(val) {
return Number(parseFloat(val)) == val;
}
if (!IsNumeric(newMaxLayers)) alert("Please enter a number");
else {
maxLayers = newMaxLayers;
var root = myDiagram.findNodesByExample({ category: "Root" }).first();
myDiagram.clearSelection();
myDiagram.select(root);
}
}
// when a new node is selected, adjust the radial layout around the new node
function nodeSelectionChanged(node) {
var diagram = node.diagram;
if (diagram === null) return;
if (node.isSelected) {
// make this Node the root
node.category = "Root";
// determine new distances from this new root node
var results = findDistances(node);
radialLayout(node, results);
}
}
// returns a Map of Nodes with distance values
function findDistances(source) {
var diagram = source.diagram;
// keep track of distances from the source node
var distances = new go.Map(go.Node, "number");
diagram.nodes.each(function(n) {
distances.add(n, Infinity);
});
// the source node starts with distance 0
distances.add(source, 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(go.Node);
seen.add(source);
// local function for finding a Node with the smallest distance in a given collection
function leastNode(coll, distances) {
var bestdist = Infinity;
var bestnode = null;
var it = coll.iterator;
while (it.next()) {
var n = it.value;
var dist = distances.getValue(n);
if (dist < bestdist) {
bestdist = dist;
bestnode = n;
}
}
return bestnode;
}
// keep track of nodes we have finished examining;
// this avoids unnecessary traversals and helps keep the SEEN collection small
var finished = new go.Set(go.Node);
while (seen.count > 0) {
// look at the unfinished node with the shortest distance so far
var least = leastNode(seen, distances);
var leastdist = distances.getValue(least);
// by the end of this loop we will have finished examining this LEAST node
seen.remove(least);
finished.add(least);
// look at all Links connected with this node
least.linksConnected.each(function(link) {
var neighbor = link.getOtherNode(least);
// skip nodes that we have finished
if (finished.contains(neighbor)) return;
var neighbordist = distances.getValue(neighbor);
// assume "distance" along a link is unitary, but could be any non-negative number.
var dist = leastdist + 1; //Math.sqrt(least.location.distanceSquaredPoint(neighbor.location));
if (dist < neighbordist) {
// if haven't seen that node before, add it to the SEEN collection
if (neighbordist == Infinity) {
seen.add(neighbor);
}
// record the new best distance so far to that node
distances.add(neighbor, dist);
}
});
}
return distances;
}
function radialLayout(root, distances) {
root.diagram.startTransaction("radial layout");
// sort all results into Arrays of Nodes with the same distance
var nodes = {};
var maxlayer = 0;
var it = distances.iterator;
while (it.next()) {
var node = it.key;
if (node !== root) node.category = ""; // remove "Root" category from all non-root nodes
node._laid = false;
var layer = it.value;
if (layer === Infinity) continue; // Infinity used as init value (set in findDistances())
if (layer > maxlayer) maxlayer = layer;
var layernodes = nodes[layer];
if (layernodes === undefined) {
layernodes = [];
nodes[layer] = layernodes;
}
layernodes.push(node);
}
// optional: add circles in the background
// need to remove any old ones first
var gridlayer = root.diagram.findLayer("Grid");
var circles = new go.Set(go.Part);
gridlayer.parts.each(function(circle) {
if (circle.name === "CIRCLE") circles.add(circle);
});
circles.each(function(circle) {
root.diagram.remove(circle);
});
// add circles centered at the root
if (showCircles) {
var $ = go.GraphObject.make; // for conciseness in defining templates
for (var lay = 1; lay <= maxLayers; lay++) {
var radius = lay * layerThickness;
var circle =
$(go.Part,
{ name: "CIRCLE", layerName: "Grid" },
{ locationSpot: go.Spot.Center, location: new go.Point(0, 0) },
$(go.Shape, "Circle",
{ width: radius * 2, height: radius * 2 },
{ fill: "rgba(200,200,200,0.2)", stroke: null }));
node.diagram.add(circle);
}
}
// now recursively position nodes (using radlay1()), starting with the root
root.location = new go.Point(0, 0);
radlay1(root, 1, 0, 360, distances);
// finally, hide nodes with distance > maxLayers
it = distances.iterator;
while (it.next()) {
var node = it.key;
node.visible = (it.value <= maxLayers);
}
root.diagram.commitTransaction("radial layout");
}
// recursively position nodes in a radial layout
function radlay1(node, layer, angle, sweep, distances) {
if (layer > maxLayers) return; // no need to position nodes outside of maxLayers
var nodes = []; // array of all Nodes connected to 'node' in layer 'layer'
node.findNodesConnected().each(function(n) {
if (n._laid) return;
if (distances.getValue(n) === layer) nodes.push(n);
});
var found = nodes.length;
if (found === 0) return;
var radius = layer * layerThickness;
var separator = sweep / found; // distance between nodes in their sweep portion
var start = angle - sweep / 2 + separator / 2;
// for each node in this layer, place it in its correct layer and position
for (var i = 0; i < found; i++) {
var n = nodes[i];
var a = start + i * separator; // the angle to rotate the node to
// 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);
n.location = p;
n._laid = true;
// rotates the node's textblock
if (rotateText) {
n.angle = a;
var label = n.findObject("TEXTBLOCK");
if (label !== null) {
label.angle = ((a > 90 && a < 270 || a < -90) ? 180 : 0);
}
}
// keep going for all layers
radlay1(n, layer + 1, a, sweep / found, distances);
}
}
</script>
</head>
<body onload="init()">
<div id="sample">
<h3>GoJS Recentering Radial</h3>
<div id="myDiagramDiv" style="border: solid 1px black; background: white; width: 100%; height: 600px"></div>
<label for="maxLayersChanger">Max Layers</label><input type="text" id="maxLayersChanger" name="maxLayers" style="width: 50px" />
<button onclick="adjustMaxLayers()">Set Max Layers</button>
<p>
Click on a Node to center it and show its relationships.
</p>
<p>
You can set some parameters in the JavaScript code to control how many layers to show,
whether to draw the circles, and whether to rotate the text.
It is also easy to add more information to each node, including pictures,
or to put such information into <a href="../intro/toolTips.html" target="_blank">Tooltips</a>.
</p>
</div>
</body>
</html>