2019-07-03 03:36:48 +00:00
|
|
|
class Layout {
|
|
|
|
|
constructor(graph) {
|
|
|
|
|
this.graph_ = graph;
|
|
|
|
|
|
2019-07-03 19:33:10 +00:00
|
|
|
this.nodes_ = [];
|
2019-07-05 16:18:22 +00:00
|
|
|
this.nodesByPos_ = new StringMap();
|
2019-07-03 18:13:11 +00:00
|
|
|
this.nodesByGraphNode_ = new Map();
|
2019-07-07 21:27:55 +00:00
|
|
|
this.linksByPos_ = new StringMap();
|
2019-07-10 20:03:05 +00:00
|
|
|
this.labelsByPos_ = new StringMap();
|
2019-07-06 20:59:27 +00:00
|
|
|
this.links_ = [];
|
2019-07-03 18:13:11 +00:00
|
|
|
|
2019-07-03 03:36:48 +00:00
|
|
|
this.setInitialPositions();
|
2019-07-09 20:34:36 +00:00
|
|
|
this.resolveGroups();
|
2019-07-06 20:59:27 +00:00
|
|
|
this.resolveLinks();
|
2019-07-13 03:46:54 +00:00
|
|
|
this.setAffinity();
|
2019-07-03 18:13:11 +00:00
|
|
|
while (this.iterate());
|
2019-07-09 16:43:33 +00:00
|
|
|
this.addGroupPos();
|
2019-07-06 20:59:27 +00:00
|
|
|
this.drawLinks();
|
2019-07-03 18:13:11 +00:00
|
|
|
this.fixOrigin();
|
2019-07-03 03:36:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setInitialPositions() {
|
|
|
|
|
const SPACING = 4;
|
|
|
|
|
|
|
|
|
|
let maxRankNodes = 0;
|
|
|
|
|
for (let nodes of this.graph_.nodesByPageRank.values()) {
|
|
|
|
|
maxRankNodes = Math.max(maxRankNodes, nodes.length);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let ranks = Array.from(this.graph_.nodesByPageRank.keys());
|
|
|
|
|
ranks.sort((a, b) => a - b);
|
|
|
|
|
for (let r = 0; r < ranks.length; ++r) {
|
|
|
|
|
let nodes = this.graph_.nodesByPageRank.get(ranks[r]);
|
2019-07-09 18:45:48 +00:00
|
|
|
let x = r * SPACING;
|
|
|
|
|
let yRankOffset = 0 - ((nodes.length / 2) * SPACING);
|
2019-07-03 03:36:48 +00:00
|
|
|
for (let n = 0; n < nodes.length; ++n) {
|
|
|
|
|
let node = nodes[n];
|
2019-07-09 18:45:48 +00:00
|
|
|
let ySubgraphOffset = (node.subgraph * SPACING * maxRankNodes);
|
|
|
|
|
let pos = [x, (n * SPACING) + yRankOffset + ySubgraphOffset];
|
2019-07-03 19:33:10 +00:00
|
|
|
let layoutNode = new LayoutNode(node, this.nodesByPos_, pos);
|
|
|
|
|
this.nodes_.push(layoutNode);
|
|
|
|
|
this.nodesByGraphNode_.set(node, layoutNode);
|
2019-07-03 18:13:11 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
nodesFromGraphNodes(graphNodes) {
|
|
|
|
|
let nodes = [];
|
|
|
|
|
for (let graphNode of graphNodes) {
|
|
|
|
|
nodes.push(this.nodesByGraphNode_.get(graphNode));
|
|
|
|
|
}
|
|
|
|
|
return nodes;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resolveGroups() {
|
|
|
|
|
this.groups_ = [];
|
|
|
|
|
for (let group of this.graph_.groups) {
|
|
|
|
|
let nodes = this.nodesFromGraphNodes(group.nodes);
|
2019-07-05 06:10:41 +00:00
|
|
|
this.groups_.push(new LayoutGroup(group, this.nodesByPos_, nodes));
|
2019-07-03 18:13:11 +00:00
|
|
|
}
|
|
|
|
|
for (let subgraph of this.graph_.nodesBySubgraph.values()) {
|
|
|
|
|
let nodes = this.nodesFromGraphNodes(subgraph);
|
2019-07-05 06:10:41 +00:00
|
|
|
this.groups_.push(new LayoutGroup(null, this.nodesByPos_, nodes));
|
2019-07-03 18:13:11 +00:00
|
|
|
}
|
2019-07-08 03:00:31 +00:00
|
|
|
for (let labelGroup of this.graph_.nodesByLabel.values()) {
|
|
|
|
|
let nodes = this.nodesFromGraphNodes(labelGroup);
|
|
|
|
|
this.groups_.push(new LayoutGroup(null, this.nodesByPos_, nodes));
|
|
|
|
|
}
|
2019-07-03 18:13:11 +00:00
|
|
|
}
|
|
|
|
|
|
2019-07-06 20:59:27 +00:00
|
|
|
resolveLinks() {
|
|
|
|
|
for (let node of this.nodes_) {
|
|
|
|
|
node.resolveLinks(this.nodesByGraphNode_);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-13 03:46:54 +00:00
|
|
|
setAffinity() {
|
2019-07-06 20:59:27 +00:00
|
|
|
for (let node of this.nodes_) {
|
2019-07-13 03:46:54 +00:00
|
|
|
node.setAffinity(this.nodesByGraphNode_);
|
2019-07-03 18:13:11 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
iterate() {
|
2019-07-03 19:33:10 +00:00
|
|
|
let objects = Array.from(this.nodes_);
|
2019-07-03 18:13:11 +00:00
|
|
|
this.setTension(objects);
|
|
|
|
|
this.sortByMostTension(objects);
|
2019-07-03 20:00:05 +00:00
|
|
|
for (let group of this.groups_) {
|
|
|
|
|
// Groups go in the list after nodes, and nodes must have tension set
|
|
|
|
|
// properly first.
|
|
|
|
|
group.setTension();
|
|
|
|
|
objects.push(group);
|
|
|
|
|
}
|
2019-07-03 18:13:11 +00:00
|
|
|
|
2019-07-05 16:41:12 +00:00
|
|
|
let baseTension = this.getTotalTension(objects);
|
2019-07-03 18:13:11 +00:00
|
|
|
for (let obj of objects) {
|
2019-07-05 16:18:22 +00:00
|
|
|
let offsets = new StringMap();
|
2019-07-03 18:13:11 +00:00
|
|
|
let addOffset = (x, y) => {
|
|
|
|
|
if (x == 0 && y == 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2019-07-05 16:18:22 +00:00
|
|
|
offsets.set([x, y], [x, y]);
|
2019-07-03 18:13:11 +00:00
|
|
|
};
|
2019-07-05 16:41:12 +00:00
|
|
|
|
|
|
|
|
// Map remembers insertion order. We do a relatively exhaustive offset
|
|
|
|
|
// search, but we short circuit, so try the most likely offset first.
|
|
|
|
|
addOffset(Math.sign(obj.vec[0]), Math.sign(obj.vec[1]));
|
2019-07-03 18:13:11 +00:00
|
|
|
for (let dir of [-1, 0, 1]) {
|
|
|
|
|
addOffset(Math.sign(obj.vec[0]), dir);
|
|
|
|
|
addOffset(dir, Math.sign(obj.vec[1]));
|
|
|
|
|
}
|
2019-07-05 16:41:12 +00:00
|
|
|
|
2019-07-03 18:13:11 +00:00
|
|
|
for (let offset of offsets.values()) {
|
|
|
|
|
if (obj.offsetCollides(offset)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
obj.savePos();
|
|
|
|
|
obj.moveBy(offset);
|
2019-07-03 18:35:24 +00:00
|
|
|
this.setTension(objects);
|
2019-07-03 18:13:11 +00:00
|
|
|
let testTension = this.getTotalTension(objects);
|
|
|
|
|
obj.restorePos();
|
2019-07-05 16:41:12 +00:00
|
|
|
if (testTension < baseTension) {
|
|
|
|
|
obj.moveBy(offset);
|
|
|
|
|
return true;
|
2019-07-03 18:13:11 +00:00
|
|
|
}
|
2019-07-03 03:36:48 +00:00
|
|
|
}
|
2019-07-03 18:13:11 +00:00
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setTension(objects) {
|
|
|
|
|
for (let obj of objects) {
|
|
|
|
|
obj.setTension();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sortByMostTension(objects) {
|
|
|
|
|
objects.sort((a, b) => b.tension - a.tension);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getTotalTension(objects) {
|
|
|
|
|
let total = 0;
|
|
|
|
|
for (let obj of objects) {
|
|
|
|
|
total += obj.tension;
|
2019-07-03 03:36:48 +00:00
|
|
|
}
|
2019-07-03 18:13:11 +00:00
|
|
|
return total;
|
2019-07-03 03:36:48 +00:00
|
|
|
}
|
|
|
|
|
|
2019-07-03 18:13:11 +00:00
|
|
|
fixOrigin() {
|
2019-07-10 22:23:06 +00:00
|
|
|
let min = [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY];
|
|
|
|
|
let max = [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY];
|
2019-07-04 06:42:05 +00:00
|
|
|
for (let group of this.groups_) {
|
|
|
|
|
let [groupMin, groupMax] = group.getMinMax();
|
2019-07-03 18:13:11 +00:00
|
|
|
for (let i of [0, 1]) {
|
2019-07-04 06:42:05 +00:00
|
|
|
min[i] = Math.min(min[i], groupMin[i]);
|
|
|
|
|
max[i] = Math.max(max[i], groupMax[i]);
|
2019-07-03 18:13:11 +00:00
|
|
|
}
|
|
|
|
|
}
|
2019-07-07 21:31:59 +00:00
|
|
|
for (let link of this.links_) {
|
|
|
|
|
for (let hop of link.path) {
|
|
|
|
|
for (let i of [0, 1]) {
|
|
|
|
|
min[i] = Math.min(min[i], hop[i]);
|
|
|
|
|
max[i] = Math.max(max[i], hop[i]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-07-08 02:35:40 +00:00
|
|
|
|
2019-07-10 22:31:51 +00:00
|
|
|
// handle empty graph
|
|
|
|
|
if (min[0] == Number.POSITIVE_INFINITY) {
|
|
|
|
|
min[0] = min[1] = max[0] = max[1] = 0;
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-10 22:05:32 +00:00
|
|
|
if (this.graph_.label) {
|
2019-07-10 22:23:06 +00:00
|
|
|
min[1] -= 1;
|
2019-07-10 22:05:32 +00:00
|
|
|
}
|
|
|
|
|
|
2019-07-08 02:35:40 +00:00
|
|
|
// Set a minimum size and center the smaller graph
|
2019-07-10 22:05:32 +00:00
|
|
|
const MIN_SIZE = 7;
|
2019-07-08 02:35:40 +00:00
|
|
|
for (let i of [0, 1]) {
|
|
|
|
|
let expand = MIN_SIZE - (max[i] - min[i] + 1);
|
|
|
|
|
if (expand <= 0) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
let expandHalf = Math.floor(expand / 2);
|
|
|
|
|
min[i] -= expandHalf;
|
|
|
|
|
max[i] += (expand - expandHalf);
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-03 18:13:11 +00:00
|
|
|
// Offset is negative minimum, e.g min -1 means +1 to all values
|
2019-07-03 19:33:10 +00:00
|
|
|
for (let node of this.nodes_) {
|
2019-07-03 18:13:11 +00:00
|
|
|
for (let i of [0, 1]) {
|
|
|
|
|
node.pos[i] -= min[i];
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-07-06 20:59:27 +00:00
|
|
|
for (let link of this.links_) {
|
|
|
|
|
for (let hop of link.path) {
|
|
|
|
|
for (let i of [0, 1]) {
|
|
|
|
|
hop[i] -= min[i];
|
|
|
|
|
}
|
2019-07-05 04:59:04 +00:00
|
|
|
}
|
|
|
|
|
}
|
2019-07-03 18:13:11 +00:00
|
|
|
this.size = [
|
|
|
|
|
max[0] - min[0] + 1,
|
|
|
|
|
max[1] - min[1] + 1,
|
|
|
|
|
];
|
2019-07-03 03:36:48 +00:00
|
|
|
}
|
2019-07-03 18:27:32 +00:00
|
|
|
|
2019-07-09 16:43:33 +00:00
|
|
|
addGroupPos() {
|
|
|
|
|
for (let group of this.groups_) {
|
|
|
|
|
if (!group.hasGraphGroup()) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
let [min, max] = group.getMinMax();
|
|
|
|
|
for (let x = min[0]; x <= max[0]; ++x) {
|
|
|
|
|
for (let y = min[1]; y <= max[1]; ++y) {
|
|
|
|
|
getOrSet(this.nodesByPos_, [x, y], group);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-06 20:59:27 +00:00
|
|
|
drawLinks() {
|
2019-07-07 23:49:42 +00:00
|
|
|
let links = [];
|
|
|
|
|
for (let from of this.nodes_) {
|
2019-07-10 17:17:17 +00:00
|
|
|
for (let link of from.links) {
|
2019-07-07 23:49:42 +00:00
|
|
|
links.push({
|
|
|
|
|
from: from,
|
2019-07-10 17:17:17 +00:00
|
|
|
to: link.to,
|
2019-07-11 05:12:08 +00:00
|
|
|
id: link.id,
|
2019-07-10 17:17:17 +00:00
|
|
|
label: link.label,
|
2019-07-07 23:49:42 +00:00
|
|
|
});
|
2019-07-05 04:59:04 +00:00
|
|
|
}
|
|
|
|
|
}
|
2019-07-07 23:49:42 +00:00
|
|
|
|
|
|
|
|
// Shortest links first
|
|
|
|
|
links.sort((a, b) => (
|
|
|
|
|
this.distance(a.from.pos, a.to.pos) -
|
|
|
|
|
this.distance(b.from.pos, b.to.pos)));
|
|
|
|
|
|
|
|
|
|
for (let link of links) {
|
|
|
|
|
this.links_.push(
|
2019-07-11 05:12:08 +00:00
|
|
|
new LayoutLink(link.from, link.to, link.id, link.label,
|
2019-07-10 20:03:05 +00:00
|
|
|
this.nodesByPos_, this.linksByPos_,
|
|
|
|
|
this.labelsByPos_));
|
2019-07-07 23:49:42 +00:00
|
|
|
}
|
2019-07-10 17:17:17 +00:00
|
|
|
|
|
|
|
|
for (let link of this.links_) {
|
|
|
|
|
link.drawLabel();
|
|
|
|
|
}
|
2019-07-07 23:49:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
distance(a, b) {
|
|
|
|
|
let vec = [
|
|
|
|
|
b[0] - a[0],
|
|
|
|
|
b[1] - a[1],
|
|
|
|
|
];
|
|
|
|
|
return Math.sqrt((vec[0] * vec[0]) + (vec[1] * vec[1]));
|
2019-07-05 04:59:04 +00:00
|
|
|
}
|
|
|
|
|
|
2019-07-03 18:27:32 +00:00
|
|
|
getDrawSteps() {
|
|
|
|
|
let steps = [
|
|
|
|
|
{
|
|
|
|
|
type: 'size',
|
|
|
|
|
size: this.size,
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
2019-07-10 22:05:32 +00:00
|
|
|
if (this.graph_.label) {
|
|
|
|
|
steps.push({
|
|
|
|
|
type: 'graphLabel',
|
2019-07-13 22:30:37 +00:00
|
|
|
id: this.graph_.labelId,
|
2019-07-10 22:05:32 +00:00
|
|
|
min: [0, 0],
|
|
|
|
|
max: [this.size[0] - 1, 0],
|
|
|
|
|
label: this.graph_.label,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-03 19:33:10 +00:00
|
|
|
let nodes = Array.from(this.nodes_);
|
2019-07-03 18:27:32 +00:00
|
|
|
for (let i of [1, 0]) {
|
|
|
|
|
nodes.sort((a, b) => a.pos[i] - b.pos[i]);
|
|
|
|
|
}
|
|
|
|
|
for (let node of nodes) {
|
2019-07-04 06:42:05 +00:00
|
|
|
steps.push(node.getStep());
|
|
|
|
|
}
|
2019-07-10 22:05:32 +00:00
|
|
|
|
2019-07-04 06:42:05 +00:00
|
|
|
for (let group of this.groups_) {
|
|
|
|
|
let step = group.getStep();
|
|
|
|
|
if (step) {
|
|
|
|
|
steps.push(step);
|
|
|
|
|
}
|
2019-07-03 18:27:32 +00:00
|
|
|
}
|
2019-07-10 22:05:32 +00:00
|
|
|
|
2019-07-06 20:59:27 +00:00
|
|
|
for (let link of this.links_) {
|
|
|
|
|
steps.push(...link.getSteps());
|
|
|
|
|
}
|
2019-07-03 18:27:32 +00:00
|
|
|
|
|
|
|
|
return steps;
|
|
|
|
|
}
|
2019-07-03 03:36:48 +00:00
|
|
|
}
|
2019-07-03 18:13:11 +00:00
|
|
|
|
|
|
|
|
<!--# include file="LayoutGroup.js" -->
|
2019-07-06 20:59:27 +00:00
|
|
|
<!--# include file="LayoutLink.js" -->
|
2019-07-03 18:13:11 +00:00
|
|
|
<!--# include file="LayoutNode.js" -->
|