gojs
Version:
Interactive diagrams, charts, and graphs, such as trees, flowcharts, orgcharts, UML, BPMN, or business diagrams
304 lines (280 loc) • 11.5 kB
HTML
<html>
<head>
<title>Radial Partition Layout</title>
<meta name="description" content="Arrange people in rings around a central person, in layers according to distance from the central person." />
<!-- 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 maxLayers = 5; // how many concentric layers or rings to show, at maximum
var layerThickness = 70; // 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
});
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: "TEXTBLOCK", // Node.location is the center of the TextBlock
selectionAdorned: false,
click: nodeClicked,
toolTip: commonToolTip
},
new go.Binding("angle"),
$(go.Shape,
{
name: "SHAPE",
fill: "lightgray", // default value, but also data-bound
strokeWidth: 0,
portId: "" // so links will go to the shape, not the whole node
},
new go.Binding("geometry", "", makeAnnularWedge),
new go.Binding("fill", "color")),
$(go.TextBlock,
{
name: "TEXTBLOCK",
width: layerThickness,
alignment: go.Spot.Right,
alignmentFocus: go.Spot.Right,
textAlign: "center"
},
new go.Binding("text"))
);
function makeAnnularWedge(data) {
var sweep = data.sweep;
var radius = data.radius;
var p = new go.Point(radius+layerThickness, 0).rotate(-sweep/2);
var q = new go.Point(radius, 0).rotate(sweep/2);
var geo = new go.Geometry()
.add(new go.PathFigure(p.x, p.y)
.add(new go.PathSegment(go.PathSegment.Arc, -sweep/2, sweep, 0, 0, radius+layerThickness, radius+layerThickness))
.add(new go.PathSegment(go.PathSegment.Line, q.x, q.y))
.add(new go.PathSegment(go.PathSegment.Arc, sweep/2, -sweep, 0, 0, radius, radius).close()));
geo.normalize();
return geo;
}
// 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,
toolTip: commonToolTip,
click: nodeClicked,
width: layerThickness * 2,
height: layerThickness*2
},
$(go.Shape, "Circle",
{ fill: "white", strokeWidth: 0, spot1: go.Spot.TopLeft, spot2: go.Spot.BottomRight }),
$(go.TextBlock,
{ font: "bold 14pt sans-serif", textAlign: "center" },
new go.Binding("text"))
));
// define the Link template -- don't show anything!
myDiagram.linkTemplate =
$(go.Link);
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);
somenode.click(null, somenode);
}
function nodeClicked(e, node) {
var diagram = node.diagram;
if (diagram === null) return;
// 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;
if (layer > maxlayer) maxlayer = layer;
var layernodes = nodes[layer];
if (layernodes === undefined) {
layernodes = [];
nodes[layer] = layernodes;
}
layernodes.push(node);
}
root.diagram.model.setDataProperty(root.data, "angle", 0);
root.diagram.model.setDataProperty(root.data, "sweep", 360);
root.diagram.model.setDataProperty(root.data, "radius", 0);
// now recursively position nodes, 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");
}
function radlay1(node, layer, angle, sweep, distances) {
if (layer > maxLayers) return;
var nodes = [];
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;
var start = angle - sweep / 2 + separator / 2;
for (var i = 0; i < found; i++) {
var n = nodes[i];
var a = start + i * separator;
var p = new go.Point(radius+layerThickness/2, 0);
p.rotate(a);
n.location = p;
n._laid = true;
n.diagram.model.setDataProperty(n.data, "angle", a);
n.diagram.model.setDataProperty(n.data, "sweep", separator);
n.diagram.model.setDataProperty(n.data, "radius", radius);
// make sure text is never upside down
var label = n.findObject("TEXTBLOCK");
if (label !== null) {
label.angle = ((a > 90 && a < 270 || a < -90) ? 180 : 0);
}
radlay1(n, layer + 1, a, sweep / found, distances);
}
}
</script>
</head>
<body onload="init()">
<div id="sample">
<div id="myDiagramDiv" style="border: solid 1px black; background: white; width: 100%; height: 600px"></div>
<p>
Click on a Node to center it and show its relationships.
</p>
</div>
</body>
</html>