UNPKG

cytoscape-tidytree

Version:

Cytoscape.js layout extension for positioning trees

146 lines (145 loc) 6.67 kB
import { Layout } from "./alg/layout.js"; class DefaultOptions { //** 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 = undefined; // 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 = undefined; // 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 = undefined; // pan to a specified position, ignored if fit is enabled zoom = undefined; // 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 = undefined; // easing of animation // returns true for nodes that should be animated, or false when the position should be set immediately animateFilter = () => true; ready = undefined; // callback for the start of the layout stop = undefined; // 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 } // using ES5 function "class" instead of ES6 class because of cytoscape calling CyLayout.call(this) internally export function CyLayout(options) { this.options = { ...new DefaultOptions(), ...options, }; } // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access 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 = 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()] === undefined ? undefined : ys[node.id()] - dims.h / 2, }; node.scratch("tidytree", data); roots.add(data); } const comp = this.options.edgeComparator; const edges = comp === undefined ? eles.edges() : eles.edges().sort(comp); for (const edge of edges) { const sourceData = edge.source().scratch("tidytree"); const targetData = edge.target().scratch("tidytree"); // ignore the edge if target already has a parent if (roots.has(targetData)) { sourceData.children.push(targetData); roots.delete(targetData); } } // if there are no roots, choose the first node as the root if (roots.size === 0) { const fakeRoot = eles.nodes().first(); const rootData = fakeRoot.scratch("tidytree"); roots.add(rootData); // remove root from its parent's children list 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; }; // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access 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) { // casts because of wrong types in @types/cytoscape // - first argument should be the layout object, not string // - LayoutPositionOptions' "ready" and "stop" properties should be callback functions, not undefined 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: treeData, tree: tree }; };