diff --git a/index.d.ts b/index.d.ts index f8e33f04b..0b153a304 100644 --- a/index.d.ts +++ b/index.d.ts @@ -3247,6 +3247,7 @@ declare namespace cytoscape { * A boolean indicating whether the algorithm should only go along edges from source to target (default false). */ directed?: boolean; + direction?: 'top-bottom' | 'bottom-top' | 'left-right' | 'right-left'; } interface SearchFirstOptions1 extends SearchFirstOptionsBase { /** diff --git a/src/extensions/layout/breadthfirst.mjs b/src/extensions/layout/breadthfirst.mjs index 377bed6f9..3293b9960 100644 --- a/src/extensions/layout/breadthfirst.mjs +++ b/src/extensions/layout/breadthfirst.mjs @@ -6,6 +6,7 @@ import * as is from '../../is.mjs'; const defaults = { fit: true, // whether to fit the viewport to the graph directed: false, // whether the tree is directed downwards (or edges can point in any direction if false) + direction: 'top-bottom', // 'top-bottom', 'bottom-top', 'left-right', or 'right-left' padding: 30, // padding on fit circle: false, // put depths in concentric circles if true, put depths top down if false grid: false, // whether to create an even grid into which the DAG is placed (circle:false only) @@ -48,10 +49,8 @@ BreadthFirstLayout.prototype.run = function(){ const maximal = options.acyclic || options.maximal || options.maximalAdjustments > 0; // maximalAdjustments for compat. w/ old code; also, setting acyclic to true sets maximal to true const hasBoundingBox = !!options.boundingBox; - const cyExtent = cy.extent(); - const bb = math.makeBoundingBox( hasBoundingBox ? options.boundingBox : { - x1: cyExtent.x1, y1: cyExtent.y1, w: cyExtent.w, h: cyExtent.h - } ); + const bb = math.makeBoundingBox( hasBoundingBox ? options.boundingBox : + structuredClone(cy.extent())); let roots; if( is.elementOrCollection( options.roots ) ){ @@ -350,7 +349,7 @@ BreadthFirstLayout.prototype.run = function(){ const maxDepthSize = depths.reduce( (max, eles) => Math.max(max, eles.length), 0 ); - const getPosition = function( ele ){ + const getPositionTopBottom = function( ele ){ const { depth, index } = getInfo( ele ); if ( options.circle ){ @@ -386,10 +385,18 @@ BreadthFirstLayout.prototype.run = function(){ return epos; } - }; - eles.nodes().layoutPositions( this, options, getPosition ); + const rotateDegrees = { + 'bottom-top': 180, + 'left-right': -90, + 'right-left': 90, + 'top-bottom': 0, + } + + const getPosition = (ele) => util.rotatePosAndSkewByBox(getPositionTopBottom(ele), bb, rotateDegrees[options.direction] ?? 0); + + eles.nodes().layoutPositions( this, options, getPosition); return this; // chaining }; diff --git a/src/util/index.mjs b/src/util/index.mjs index 3bdb901fa..07978f619 100644 --- a/src/util/index.mjs +++ b/src/util/index.mjs @@ -10,6 +10,7 @@ export * from './maps.mjs'; export * from './strings.mjs'; export * from './timing.mjs'; export * from './hash.mjs'; +export * from './position.mjs'; export { strings, extend, extend as assign, memoize, regex, sort }; diff --git a/src/util/position.mjs b/src/util/position.mjs new file mode 100644 index 000000000..54f94e993 --- /dev/null +++ b/src/util/position.mjs @@ -0,0 +1,33 @@ +export function rotatePoint(x, y, centerX, centerY, angleDegrees) { + const angleRadians = (angleDegrees * Math.PI) / 180; + const rotatedX = + Math.cos(angleRadians) * (x - centerX) - + Math.sin(angleRadians) * (y - centerY) + + centerX; + const rotatedY = + Math.sin(angleRadians) * (x - centerX) + + Math.cos(angleRadians) * (y - centerY) + + centerY; + return { x: rotatedX, y: rotatedY }; +} + +export const skewPointInBox = (x, y, boxX, boxY, skewX, skewY) => ({ + x: (x - boxX) * skewX + boxX, + y: (y - boxY) * skewY + boxY +}); + +export function rotatePosAndSkewByBox(pos, box, angleDegrees) { + if (angleDegrees === 0) return pos; + const centerX = (box.x1 + box.x2) / 2; + const centerY = (box.y1 + box.y2) / 2; + const skewX = box.w / box.h; + const skewY = 1 / skewX; + + const rotated = rotatePoint(pos.x, pos.y, centerX, centerY, angleDegrees); + const skewed = skewPointInBox(rotated.x, rotated.y, centerX, centerY, skewX, skewY); + + return { + x: skewed.x, + y: skewed.y, + }; +}; \ No newline at end of file