2019-07-03 18:13:11 +00:00
|
|
|
class LayoutNode {
|
|
|
|
|
constructor(graphNode, nodesByPos, pos) {
|
2019-07-04 06:42:05 +00:00
|
|
|
this.graphNode_ = graphNode;
|
2019-07-03 18:13:11 +00:00
|
|
|
this.nodesByPos_ = nodesByPos;
|
|
|
|
|
this.pos = pos;
|
2019-07-13 03:34:34 +00:00
|
|
|
|
2019-07-10 08:27:07 +00:00
|
|
|
this.groups = new Set();
|
2019-07-14 20:44:07 +00:00
|
|
|
this.tags = new Set();
|
2019-07-13 03:34:34 +00:00
|
|
|
this.affinity_ = [];
|
|
|
|
|
|
|
|
|
|
this.label = this.graphNode_.label;
|
2019-07-13 03:41:36 +00:00
|
|
|
this.pageRank = this.graphNode_.pageRank;
|
2019-07-13 03:34:34 +00:00
|
|
|
this.subgraph = this.graphNode_.subgraph;
|
2019-07-03 18:13:11 +00:00
|
|
|
|
2019-07-05 16:18:22 +00:00
|
|
|
this.nodesByPos_.set(this.pos, this);
|
2019-07-03 18:13:11 +00:00
|
|
|
}
|
|
|
|
|
|
2019-07-06 20:59:27 +00:00
|
|
|
resolveLinks(nodesByGraphNode) {
|
|
|
|
|
this.links = [];
|
2019-07-10 17:17:17 +00:00
|
|
|
for (let link of this.graphNode_.links) {
|
|
|
|
|
this.links.push({
|
|
|
|
|
to: nodesByGraphNode.get(link.to),
|
2019-07-13 21:44:51 +00:00
|
|
|
id: link.id,
|
2019-07-10 17:17:17 +00:00
|
|
|
label: link.label,
|
2019-07-14 03:19:15 +00:00
|
|
|
labelId: link.labelId,
|
2019-07-10 17:17:17 +00:00
|
|
|
});
|
2019-07-06 20:59:27 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-13 03:46:54 +00:00
|
|
|
setAffinity(nodesByGraphNode) {
|
2019-07-09 20:34:36 +00:00
|
|
|
const INF = 999999;
|
|
|
|
|
|
|
|
|
|
for (let node of nodesByGraphNode.values()) {
|
2019-07-13 03:34:34 +00:00
|
|
|
// Weak affinity full mesh
|
|
|
|
|
// Keep unassociated subgroups together
|
|
|
|
|
this.addAffinity(node, d => d);
|
|
|
|
|
|
|
|
|
|
// Keep one space between subgraphs
|
|
|
|
|
if (this.subgraph != node.subgraph && this.label != node.label) {
|
|
|
|
|
this.addAffinity(node, d => d <= 2 ? -INF : 0);
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-13 03:41:36 +00:00
|
|
|
// Am I in any labeled groups that node is not?
|
2019-07-16 04:55:01 +00:00
|
|
|
// If so, preserve two spaces above the group
|
|
|
|
|
if (asymDifference(this.labelGroups(this.groups), node.groups).size) {
|
2019-07-16 04:45:12 +00:00
|
|
|
this.addAffinity(node,
|
|
|
|
|
(d, v) =>
|
|
|
|
|
(v[0] >= -1 && v[0] <= 1 && v[1] < 0 && v[1] >= -2) ? -INF : 0);
|
2019-07-13 03:41:36 +00:00
|
|
|
}
|
|
|
|
|
|
2019-07-16 04:55:01 +00:00
|
|
|
// Preserve one space all around the group
|
|
|
|
|
if (asymDifference(this.graphGroups(this.groups),
|
|
|
|
|
this.graphGroups(node.groups)).size) {
|
2019-07-16 04:46:30 +00:00
|
|
|
this.addAffinity(node, d => d <= 2 ? -INF : 0);
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-13 03:41:36 +00:00
|
|
|
// Try to stack nodes with the same label
|
|
|
|
|
if (node.label == this.label) {
|
|
|
|
|
this.addAffinity(node, (d, v) => v[0] == 0 ? 200 : 500);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Try to preserve pagerank left-to-right flow from initial positions
|
|
|
|
|
let rankSign = Math.sign(node.pageRank - this.pageRank);
|
|
|
|
|
if (rankSign != 0) {
|
|
|
|
|
this.addAffinity(node, (d, v) =>
|
|
|
|
|
[Math.sign(v[0]) == rankSign ? 0 : -1000, 0]);
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-09 20:34:36 +00:00
|
|
|
for (let group of this.groups) {
|
|
|
|
|
// Ensure groups do not overlap
|
|
|
|
|
if (group.nodes.has(node)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2019-07-15 23:58:23 +00:00
|
|
|
this.addAffinity(node, (d, v, p) => group.isContained(p) ? -INF : 0);
|
2019-07-09 20:34:36 +00:00
|
|
|
}
|
|
|
|
|
}
|
2019-07-13 03:46:54 +00:00
|
|
|
|
|
|
|
|
for (let link of this.links) {
|
|
|
|
|
// Stronger affinity for links
|
|
|
|
|
// Prefer to move toward the target instance
|
|
|
|
|
this.addAffinity(link.to, d => d <= 2 ? -INF : d * 11);
|
|
|
|
|
link.to.addAffinity(this, d => d <= 2 ? -INF : d * 9);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Affinity for groups
|
|
|
|
|
for (let group of this.groups) {
|
|
|
|
|
if (!group.hasGraphGroup()) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
for (let node of group.nodes) {
|
|
|
|
|
this.addAffinity(node, d => d * 100);
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-07-03 18:13:11 +00:00
|
|
|
}
|
|
|
|
|
|
2019-07-16 04:55:01 +00:00
|
|
|
graphGroups(groups) {
|
|
|
|
|
return new Set(Array.from(groups).filter(g => g.hasGraphGroup()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
labelGroups(groups) {
|
|
|
|
|
return new Set(Array.from(groups).filter(g => !!g.label));
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-13 03:34:34 +00:00
|
|
|
addAffinity(node, distanceToWeight) {
|
|
|
|
|
if (this == node) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this.affinity_.push({
|
|
|
|
|
node: node,
|
|
|
|
|
distanceToWeight: distanceToWeight,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-03 18:13:11 +00:00
|
|
|
setTension() {
|
|
|
|
|
this.vec = [0, 0];
|
|
|
|
|
this.tension = 0;
|
|
|
|
|
for (let aff of this.affinity_) {
|
|
|
|
|
let vec = [], vecsum = 0;
|
|
|
|
|
for (let i of [0, 1]) {
|
|
|
|
|
vec[i] = aff.node.pos[i] - this.pos[i];
|
|
|
|
|
vecsum += Math.abs(vec[i]);
|
2019-07-13 05:10:56 +00:00
|
|
|
}
|
2019-07-03 20:00:05 +00:00
|
|
|
// Avoid calling sqrt(), since the results are used relatively
|
|
|
|
|
let distanceSquared = vec[0] * vec[0] + vec[1] * vec[1];
|
2019-07-09 20:34:36 +00:00
|
|
|
let weight = aff.distanceToWeight(distanceSquared, vec, aff.node.pos);
|
2019-07-09 05:36:32 +00:00
|
|
|
if (weight instanceof Array) {
|
|
|
|
|
for (let i of [0, 1]) {
|
|
|
|
|
this.vec[i] += weight[i];
|
|
|
|
|
this.tension += Math.abs(weight[i]);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
for (let i of [0, 1]) {
|
|
|
|
|
this.vec[i] += (weight * vec[i]) / vecsum;
|
|
|
|
|
}
|
|
|
|
|
this.tension += Math.abs(weight);
|
2019-07-03 18:13:11 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
offsetToPos(offset) {
|
|
|
|
|
return [
|
|
|
|
|
this.pos[0] + offset[0],
|
|
|
|
|
this.pos[1] + offset[1],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
offsetCollides(offset) {
|
|
|
|
|
let newPos = this.offsetToPos(offset);
|
2019-07-05 16:18:22 +00:00
|
|
|
return this.nodesByPos_.get(newPos);
|
2019-07-03 18:13:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
moveTo(pos) {
|
2019-07-05 16:18:22 +00:00
|
|
|
this.nodesByPos_.delete(this.pos);
|
2019-07-03 18:13:11 +00:00
|
|
|
this.pos = pos;
|
2019-07-05 16:18:22 +00:00
|
|
|
this.nodesByPos_.set(this.pos, this);
|
2019-07-03 18:13:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
moveBy(offset) {
|
|
|
|
|
this.moveTo(this.offsetToPos(offset));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
savePos() {
|
|
|
|
|
this.savedPos_ = this.pos;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
restorePos() {
|
|
|
|
|
this.moveTo(this.savedPos_);
|
|
|
|
|
}
|
2019-07-04 06:42:05 +00:00
|
|
|
|
|
|
|
|
getStep() {
|
|
|
|
|
return {
|
|
|
|
|
type: 'node',
|
|
|
|
|
pos: this.pos,
|
2019-07-11 05:12:08 +00:00
|
|
|
id: this.graphNode_.id,
|
2019-07-04 06:42:05 +00:00
|
|
|
label: this.graphNode_.label,
|
2019-07-14 20:44:07 +00:00
|
|
|
tags: Array.from(this.tags),
|
2019-07-04 06:42:05 +00:00
|
|
|
};
|
|
|
|
|
}
|
2019-07-03 18:13:11 +00:00
|
|
|
}
|