cytoscape-tidytree
Version:
Cytoscape.js layout extension for positioning trees
388 lines (384 loc) • 12.3 kB
JavaScript
"use strict";
(() => {
// src/alg/tree.ts
var IYL = class _IYL {
lowY;
index;
nxt;
constructor(lowY, index, nxt) {
this.lowY = lowY;
this.index = index;
this.nxt = nxt;
}
static updateIYL(minY, i, ih) {
while (ih != void 0 && minY >= ih.lowY)
ih = ih.nxt;
return new _IYL(minY, i, ih);
}
};
var Tree = class {
isExtra;
w;
h;
x;
y;
c;
prelim = 0;
mod = 0;
shift = 0;
change = 0;
tl;
tr;
// Left and right thread.
el;
er;
// Extreme left and right nodes.
msel;
mser;
// Sum of modifiers at the extreme nodes.
constructor(w, h, y, c, isExtra = false) {
this.w = w;
this.h = h;
this.y = y;
this.c = c;
this.isExtra = isExtra;
}
layout() {
this.firstWalk();
this.secondWalk(0);
}
firstWalk() {
if (this.c.length == 0) {
this.setExtremes();
return;
}
this.c[0].firstWalk();
let ih = IYL.updateIYL(this.c[0].el.bottom(), 0, void 0);
for (let i = 1; i < this.c.length; i++) {
this.c[i].firstWalk();
const minY = this.c[i].er.bottom();
this.separate(i, ih);
ih = IYL.updateIYL(minY, i, ih);
}
this.positionRoot();
this.setExtremes();
}
setExtremes() {
if (this.c.length == 0) {
this.el = this;
this.er = this;
this.msel = this.mser = 0;
} else {
this.el = this.c[0].el;
this.msel = this.c[0].msel;
this.er = this.c[this.c.length - 1].er;
this.mser = this.c[this.c.length - 1].mser;
}
}
separate(i, ih) {
let sr = this.c[i - 1];
let mssr = sr.mod;
let cl = this.c[i];
let mscl = cl.mod;
let first = true;
while (sr !== void 0 && cl !== void 0) {
if (sr.bottom() > ih.lowY)
ih = ih.nxt;
const dist = mssr + sr.prelim + sr.w - (mscl + cl.prelim);
if (dist > 0 || first && dist <= 0) {
mscl += dist;
this.moveSubtree(i, ih.index, dist);
}
first = false;
const sy = sr.bottom(), cy = cl.bottom();
if (sy <= cy) {
sr = sr.nextRightContour();
if (sr !== void 0)
mssr += sr.mod;
}
if (sy >= cy) {
cl = cl.nextLeftContour();
if (cl !== void 0)
mscl += cl.mod;
}
}
if (sr == void 0 && cl != void 0)
this.setLeftThread(i, cl, mscl);
else if (sr != void 0 && cl == void 0)
this.setRightThread(i, sr, mssr);
}
moveSubtree(i, si, dist) {
this.c[i].mod += dist;
this.c[i].msel += dist;
this.c[i].mser += dist;
this.distributeExtra(i, si, dist);
}
nextLeftContour() {
return this.c.length == 0 ? this.tl : this.c[0];
}
nextRightContour() {
return this.c.length == 0 ? this.tr : this.c[this.c.length - 1];
}
bottom() {
return this.y + this.h;
}
setLeftThread(i, cl, modsumcl) {
const li = this.c[0].el;
li.tl = cl;
const diff = modsumcl - cl.mod - this.c[0].msel;
li.mod += diff;
li.prelim -= diff;
this.c[0].el = this.c[i].el;
this.c[0].msel = this.c[i].msel;
}
// Symmetrical to setLeftThread.
setRightThread(i, sr, modsumsr) {
const ri = this.c[i].er;
ri.tr = sr;
const diff = modsumsr - sr.mod - this.c[i].mser;
ri.mod += diff;
ri.prelim -= diff;
this.c[i].er = this.c[i - 1].er;
this.c[i].mser = this.c[i - 1].mser;
}
positionRoot() {
this.prelim = (this.c[0].prelim + this.c[0].mod + this.c[this.c.length - 1].mod + this.c[this.c.length - 1].prelim + this.c[this.c.length - 1].w) / 2 - this.w / 2;
}
secondWalk(modsum) {
modsum += this.mod;
this.x = this.prelim + modsum;
this.addChildSpacing();
for (const child of this.c)
child.secondWalk(modsum);
}
distributeExtra(i, si, dist) {
if (si != i - 1) {
const nr = i - si;
this.c[si + 1].shift += dist / nr;
this.c[i].shift -= dist / nr;
this.c[i].change -= dist - dist / nr;
}
}
// Process change and shift to add intermediate spacing to mod.
addChildSpacing() {
let d = 0, modsumdelta = 0;
for (const child of this.c) {
d += child.shift;
modsumdelta += d + child.change;
child.mod += modsumdelta;
}
}
};
// src/alg/layout.ts
var Layout = class {
options;
constructor(options) {
this.options = {
horizontalSpacing: 10,
verticalSpacing: 20,
lineWidth: 3,
layerHeight: void 0,
...options
};
}
run(data) {
const tree = this.makeTree(data, data.customY);
tree.layout();
this.setLayoutResult(data, tree);
return tree;
}
makeTree(data, root_y = 0) {
const outerWidth = data.w + this.options.horizontalSpacing;
let outerHeight = data.h + this.options.verticalSpacing;
let extraSpacing = data.extraVerticalSpacing ?? 0;
const layerHeight = this.options.layerHeight;
if (layerHeight !== void 0) {
outerHeight = Math.ceil(outerHeight / layerHeight) * layerHeight;
extraSpacing = Math.round(extraSpacing / layerHeight) * layerHeight;
}
if (data.customY !== void 0) {
extraSpacing = data.customY - root_y;
data.y = root_y + extraSpacing;
}
data.y = root_y + extraSpacing;
const vertex = new Tree(
outerWidth,
outerHeight,
data.y,
data.children.map((child) => this.makeTree(child, data.y + outerHeight))
);
if (extraSpacing < 0) {
console.warn("Node has negative extra space, ignoring", data);
}
if (extraSpacing > 0) {
return new Tree(this.options.lineWidth, extraSpacing, root_y, [vertex], true);
}
return vertex;
}
setLayoutResult(data, tree) {
if (tree.isExtra) {
tree = tree.c[0];
}
data.x = tree.x + this.options.horizontalSpacing / 2;
data.children.forEach((child, i) => this.setLayoutResult(child, tree.c[i]));
}
};
// src/cy_layout.ts
var DefaultOptions = class {
//** Needed to for the layout to be called from cytoscape */
name = "tidytree";
/**
* Specific layout options
*/
dataOnly = false;
// when enabled, nodes' positions aren't set
horizontalSpacing = 20;
// the width of the space between nodes in cytoscape units
verticalSpacing = 40;
// the height of the space between parent and child in cytoscape units
direction = "TB";
// the direction of the tree, left to right, right to left, top to bottom, bottom to top
// a map from node's id to how much space should be added between it and its parent
extraVerticalSpacings = {};
// a map from node's id to how much space should be added for the node to have this y position
// overrides extraVerticalSpacings if both are set for a particular node
// if the y position would result in the child not being below the parent, the setting is ignored and a warning is printed
customYs = {};
// the width of the space left after a node is moved down
lineWidth = 5;
// forces nodes to be positioned on multiples of this value if set
layerHeight = void 0;
// a sorting function for the children array of the tree representation
// if undefined, the order is based on the order of the collection the layout was called on
edgeComparator = void 0;
// when not changed, the width and height of each node is read directly from the node
// this parameter allows to supply your own sizes
// if the h or w property is missing from the returned object, it is taken from the node
sizeGetter = () => ({});
/**
* Layout options passed to nodes.layoutPositions()
* https://js.cytoscape.org/#nodes.layoutPositions
*/
fit = true;
// if true, fits the viewport to the graph
padding = 30;
// the padding between the viewport and the graph on fit
pan = void 0;
// pan to a specified position, ignored if fit is enabled
zoom = void 0;
// how much to zoom the viewport, ignored if fit is enabled
// a positive value which adjusts spacing between nodes (>1 means greater than usual spacing)
spacingFactor = 1;
// allows to transform a given node's position before it is applied
transform = (n, p) => p;
animate = false;
// animate the layout`s changes
animationDuration = 500;
// duration of the animation in ms
animationEasing = void 0;
// easing of animation
// returns true for nodes that should be animated, or false when the position should be set immediately
animateFilter = () => true;
ready = void 0;
// callback for the start of the layout
stop = void 0;
// callback for the layout`s finish
/**
* Layout options passed to nodes.node.layoutDimensions()
* https://js.cytoscape.org/#node.layoutDimensions
*/
nodeDimensionsIncludeLabels = true;
// if overflowing labels shoud count in the width or height of the node
};
function CyLayout(options) {
this.options = {
...new DefaultOptions(),
...options
};
}
CyLayout.prototype.createTreeData = function() {
const includeLabels = this.options.nodeDimensionsIncludeLabels ?? true;
const eles = this.options.eles;
const ys = this.options.customYs;
const vertSpaces = this.options.extraVerticalSpacings;
const roots = /* @__PURE__ */ new Set();
for (const node of eles.nodes()) {
const dims = {
...node.layoutDimensions({ nodeDimensionsIncludeLabels: includeLabels }),
...this.options.sizeGetter(node)
};
if (this.options.direction === "LR" || this.options.direction === "RL") {
[dims.w, dims.h] = [dims.h, dims.w];
}
const data = {
id: node.id(),
w: dims.w,
h: dims.h,
children: [],
extraVerticalSpacing: vertSpaces[node.id()],
customY: ys[node.id()] === void 0 ? void 0 : ys[node.id()] - dims.h / 2
};
node.scratch("tidytree", data);
roots.add(data);
}
const comp = this.options.edgeComparator;
const edges = comp === void 0 ? eles.edges() : eles.edges().sort(comp);
for (const edge of edges) {
const sourceData = edge.source().scratch("tidytree");
const targetData = edge.target().scratch("tidytree");
if (roots.has(targetData)) {
sourceData.children.push(targetData);
roots.delete(targetData);
}
}
if (roots.size === 0) {
const fakeRoot = eles.nodes().first();
const rootData = fakeRoot.scratch("tidytree");
roots.add(rootData);
for (const parent of fakeRoot.incomers("node")) {
const sourceData = parent.scratch("tidytree");
const i = sourceData.children.indexOf(rootData);
if (i !== -1) {
sourceData.children.splice(i, 1);
}
}
}
const newRoot = {
w: 0,
h: 0,
children: Array.from(roots),
customY: Math.min(-this.options.verticalSpacing, -(this.options.layerHeight ?? 0))
};
return newRoot;
};
CyLayout.prototype.run = function() {
const treeData = this.createTreeData();
const tree = new Layout(this.options).run(treeData);
const nodes = this.options.eles.nodes();
if (!this.options.dataOnly) {
nodes.layoutPositions(this, this.options, (node) => {
const data = node.scratch("tidytree");
const pos = { x: data.x + data.w / 2, y: data.y + data.h / 2 };
if (this.options.direction === "LR" || this.options.direction === "RL") {
[pos.x, pos.y] = [pos.y, pos.x];
}
if (this.options.direction === "BT") {
pos.y = -pos.y;
}
if (this.options.direction === "RL") {
pos.x = -pos.x;
}
return pos;
});
}
return { treeData, tree };
};
// src/index.ts
function register(cytoscape) {
cytoscape("layout", "tidytree", CyLayout);
}
if (typeof window.cytoscape !== "undefined") {
register(window.cytoscape);
}
})();
//# sourceMappingURL=cytoscape-tidytree.js.map