/**
|
|
* Helper classes for LayoutEngine.
|
|
*
|
|
* Strategy pattern for usage of direction methods for hierarchical layouts.
|
|
*/
|
|
var TimSort = require('timsort');
|
|
|
|
|
|
/**
|
|
* Interface definition for direction strategy classes.
|
|
*
|
|
* This class describes the interface for the Strategy
|
|
* pattern classes used to differentiate horizontal and vertical
|
|
* direction of hierarchical results.
|
|
*
|
|
* For a given direction, one coordinate will be 'fixed', meaning that it is
|
|
* determined by level.
|
|
* The other coordinate is 'unfixed', meaning that the nodes on a given level
|
|
* can still move along that coordinate. So:
|
|
*
|
|
* - `vertical` layout: `x` unfixed, `y` fixed per level
|
|
* - `horizontal` layout: `x` fixed per level, `y` unfixed
|
|
*
|
|
* The local methods are stubs and should be regarded as abstract.
|
|
* Derived classes **must** implement all the methods themselves.
|
|
*
|
|
* @private
|
|
*/
|
|
class DirectionInterface {
|
|
/** @ignore **/
|
|
abstract() {
|
|
throw new Error("Can't instantiate abstract class!");
|
|
}
|
|
|
|
/**
|
|
* This is a dummy call which is used to suppress the jsdoc errors of type:
|
|
*
|
|
* "'param' is assigned a value but never used"
|
|
*
|
|
* @ignore
|
|
**/
|
|
fake_use() {
|
|
// Do nothing special
|
|
}
|
|
|
|
/**
|
|
* Type to use to translate dynamic curves to, in the case of hierarchical layout.
|
|
* Dynamic curves do not work for these.
|
|
*
|
|
* The value should be perpendicular to the actual direction of the layout.
|
|
*
|
|
* @return {string} Direction, either 'vertical' or 'horizontal'
|
|
*/
|
|
curveType() { return this.abstract(); }
|
|
|
|
|
|
/**
|
|
* Return the value of the coordinate that is not fixed for this direction.
|
|
*
|
|
* @param {Node} node The node to read
|
|
* @return {number} Value of the unfixed coordinate
|
|
*/
|
|
getPosition(node) { this.fake_use(node); return this.abstract(); }
|
|
|
|
|
|
/**
|
|
* Set the value of the coordinate that is not fixed for this direction.
|
|
*
|
|
* @param {Node} node The node to adjust
|
|
* @param {number} position
|
|
* @param {number} [level] if specified, the hierarchy level that this node should be fixed to
|
|
*/
|
|
setPosition(node, position, level = undefined) { this.fake_use(node, position, level); this.abstract(); }
|
|
|
|
|
|
/**
|
|
* Get the width of a tree.
|
|
*
|
|
* A `tree` here is a subset of nodes within the network which are not connected to other nodes,
|
|
* only among themselves. In essence, it is a sub-network.
|
|
*
|
|
* @param {number} index The index number of a tree
|
|
* @return {number} the width of a tree in the view coordinates
|
|
*/
|
|
getTreeSize(index) { this.fake_use(index); return this.abstract(); }
|
|
|
|
|
|
/**
|
|
* Sort array of nodes on the unfixed coordinates.
|
|
*
|
|
* **Note:** chrome has non-stable sorting implementation, which
|
|
* has a tendency to change the order of the array items,
|
|
* even if the custom sort function returns 0.
|
|
*
|
|
* For this reason, an external sort implementation is used,
|
|
* which has the added benefit of being faster than the standard
|
|
* platforms implementation. This has been verified on `node.js`,
|
|
* `firefox` and `chrome` (all linux).
|
|
*
|
|
* @param {Array.<Node>} nodeArray array of nodes to sort
|
|
*/
|
|
sort(nodeArray) { this.fake_use(nodeArray); this.abstract(); }
|
|
|
|
|
|
/**
|
|
* Assign the fixed coordinate of the node to the given level
|
|
*
|
|
* @param {Node} node The node to adjust
|
|
* @param {number} level The level to fix to
|
|
*/
|
|
fix(node, level) { this.fake_use(node, level); this.abstract(); }
|
|
|
|
|
|
/**
|
|
* Add an offset to the unfixed coordinate of the given node.
|
|
*
|
|
* @param {NodeId} nodeId Id of the node to adjust
|
|
* @param {number} diff Offset to add to the unfixed coordinate
|
|
*/
|
|
shift(nodeId, diff) { this.fake_use(nodeId, diff); this.abstract(); }
|
|
}
|
|
|
|
|
|
/**
|
|
* Vertical Strategy
|
|
*
|
|
* Coordinate `y` is fixed on levels, coordinate `x` is unfixed.
|
|
*
|
|
* @extends DirectionInterface
|
|
* @private
|
|
*/
|
|
class VerticalStrategy extends DirectionInterface {
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @param {Object} layout reference to the parent LayoutEngine instance.
|
|
*/
|
|
constructor(layout) {
|
|
super();
|
|
this.layout = layout;
|
|
}
|
|
|
|
/** @inheritdoc */
|
|
curveType() {
|
|
return 'horizontal';
|
|
}
|
|
|
|
/** @inheritdoc */
|
|
getPosition(node) {
|
|
return node.x;
|
|
}
|
|
|
|
/** @inheritdoc */
|
|
setPosition(node, position, level = undefined) {
|
|
if (level !== undefined) {
|
|
this.layout.hierarchical.addToOrdering(node, level);
|
|
}
|
|
node.x = position;
|
|
}
|
|
|
|
/** @inheritdoc */
|
|
getTreeSize(index) {
|
|
let res = this.layout.hierarchical.getTreeSize(this.layout.body.nodes, index);
|
|
return {min: res.min_x, max: res.max_x};
|
|
}
|
|
|
|
/** @inheritdoc */
|
|
sort(nodeArray) {
|
|
TimSort.sort(nodeArray, function(a, b) {
|
|
return a.x - b.x;
|
|
});
|
|
}
|
|
|
|
/** @inheritdoc */
|
|
fix(node, level) {
|
|
node.y = this.layout.options.hierarchical.levelSeparation * level;
|
|
node.options.fixed.y = true;
|
|
}
|
|
|
|
/** @inheritdoc */
|
|
shift(nodeId, diff) {
|
|
this.layout.body.nodes[nodeId].x += diff;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Horizontal Strategy
|
|
*
|
|
* Coordinate `x` is fixed on levels, coordinate `y` is unfixed.
|
|
*
|
|
* @extends DirectionInterface
|
|
* @private
|
|
*/
|
|
class HorizontalStrategy extends DirectionInterface {
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @param {Object} layout reference to the parent LayoutEngine instance.
|
|
*/
|
|
constructor(layout) {
|
|
super();
|
|
this.layout = layout;
|
|
}
|
|
|
|
/** @inheritdoc */
|
|
curveType() {
|
|
return 'vertical';
|
|
}
|
|
|
|
/** @inheritdoc */
|
|
getPosition(node) {
|
|
return node.y;
|
|
}
|
|
|
|
/** @inheritdoc */
|
|
setPosition(node, position, level = undefined) {
|
|
if (level !== undefined) {
|
|
this.layout.hierarchical.addToOrdering(node, level);
|
|
}
|
|
node.y = position;
|
|
}
|
|
|
|
/** @inheritdoc */
|
|
getTreeSize(index) {
|
|
let res = this.layout.hierarchical.getTreeSize(this.layout.body.nodes, index);
|
|
return {min: res.min_y, max: res.max_y};
|
|
}
|
|
|
|
/** @inheritdoc */
|
|
sort(nodeArray) {
|
|
TimSort.sort(nodeArray, function(a, b) {
|
|
return a.y - b.y;
|
|
});
|
|
}
|
|
|
|
/** @inheritdoc */
|
|
fix(node, level) {
|
|
node.x = this.layout.options.hierarchical.levelSeparation * level;
|
|
node.options.fixed.x = true;
|
|
}
|
|
|
|
/** @inheritdoc */
|
|
shift(nodeId, diff) {
|
|
this.layout.body.nodes[nodeId].y += diff;
|
|
}
|
|
}
|
|
|
|
|
|
export {HorizontalStrategy, VerticalStrategy};
|