diff --git a/packages/joint-layout-directed-graph/DirectedGraph.d.ts b/packages/joint-layout-directed-graph/DirectedGraph.d.ts index c6472823b6..7a3db713e6 100644 --- a/packages/joint-layout-directed-graph/DirectedGraph.d.ts +++ b/packages/joint-layout-directed-graph/DirectedGraph.d.ts @@ -26,7 +26,7 @@ export namespace DirectedGraph { marginX?: number; marginY?: number; resizeClusters?: boolean; - clusterPadding?: dia.Padding; + clusterPadding?: dia.Padding | 'default'; debugTiming?: boolean; } @@ -34,7 +34,7 @@ export namespace DirectedGraph { setPosition?: (element: dia.Element, position: dia.BBox) => void; setVertices?: boolean | ((link: dia.Link, vertices: dia.Point[]) => void); setLabels?: boolean | ((link: dia.Link, position: dia.Point, points: dia.Point[]) => void); - // deprecated + /** @deprecated use `setVertices` instead */ setLinkVertices?: boolean; } @@ -58,12 +58,12 @@ export namespace DirectedGraph { export function fromGraphLib(glGraph: any, opt?: FromGraphLibOptions): dia.Graph; - // @deprecated pass the `graph` option instead + /** @deprecated pass the `graph` option instead */ export function fromGraphLib(this: dia.Graph, glGraph: any, opt?: { [key: string]: any }): dia.Graph; - // @deprecated use `FromGraphLibOptions` instead + /** @deprecated use `FromGraphLibOptions` instead */ type fromGraphLibOptions = FromGraphLibOptions; - // @deprecated use `ToGraphLibOptions` instead + /** @deprecated use `ToGraphLibOptions` instead */ type toGraphLibOptions = ToGraphLibOptions; } diff --git a/packages/joint-layout-directed-graph/DirectedGraph.mjs b/packages/joint-layout-directed-graph/DirectedGraph.mjs index f8fe162628..9a33bf4f54 100644 --- a/packages/joint-layout-directed-graph/DirectedGraph.mjs +++ b/packages/joint-layout-directed-graph/DirectedGraph.mjs @@ -36,10 +36,10 @@ export const DirectedGraph = { importElement: function(nodeId, glGraph, graph, opt) { - var element = graph.getCell(nodeId); - var glNode = glGraph.node(nodeId); + const element = graph.getCell(nodeId); + const glNode = glGraph.node(nodeId); - if (opt.setPosition) { + if (util.isFunction(opt.setPosition)) { opt.setPosition(element, glNode); } else { element.set('position', { @@ -47,6 +47,17 @@ export const DirectedGraph = { y: glNode.y - glNode.height / 2 }); } + + // check if we want to use Dagre's default cluster padding + if (opt.resizeClusters && (opt.clusterPadding === 'default')) { + // check if element is a cluster (clusters has no `rank` assigned) + if (glNode.rank === undefined) { + element.set('size', { + width: glNode.width, + height: glNode.height + }); + } + } // else: possibly apply numeric `opt.clusterPadding` (see `layout()` function) }, importLink: function(edgeObj, glGraph, graph, opt) { @@ -172,7 +183,7 @@ export const DirectedGraph = { graph, }); - if (opt.resizeClusters) { + if (opt.resizeClusters && (typeof opt.clusterPadding === 'number')) { // Resize and reposition cluster elements // Filter out top-level clusters (nodes without a parent and with children) and map them to cells const topLevelClusters = glGraph.nodes() diff --git a/packages/joint-layout-directed-graph/test/index.js b/packages/joint-layout-directed-graph/test/index.js index ebc2d582bf..7d621b476a 100644 --- a/packages/joint-layout-directed-graph/test/index.js +++ b/packages/joint-layout-directed-graph/test/index.js @@ -274,7 +274,6 @@ QUnit.module('DirectedGraph', function(hooks) { assert.deepEqual({ x, y }, { x: 5, y: 210 }); }); - QUnit.test('should return a rectangle representing the graph bounding box', function(assert) { var bbox; @@ -315,10 +314,48 @@ QUnit.module('DirectedGraph', function(hooks) { marginY: -1000 }); assert.deepEqual(bbox.toJSON(), graph.getBBox().toJSON()); + }); + + QUnit.test('resizeClusters: false, clusterPadding: number - should not resize clusters', function(assert) { + + const deepestSize = { + width: 500, + height: 500 + }; + + const elements = [ + new joint.shapes.standard.Rectangle({ size: { width: 60, height: 60 }}), + new joint.shapes.standard.Rectangle({ size: { width: 120, height: 120 }}), + new joint.shapes.standard.Rectangle({ size: { width: 100, height: 300 }}), + new joint.shapes.standard.Rectangle({ size: deepestSize }) + ]; + + elements[0].embed(elements[1]); + elements[1].embed(elements[2]); + elements[2].embed(elements[3]); + + graph.resetCells(elements); + + const padding = 20; + + DirectedGraph.layout(graph, { + resizeClusters: false, + clusterPadding: padding + }); + // Sizes remain unchanged + const expectedSizes = [ + { width: 60, height: 60 }, + { width: 120, height: 120 }, + { width: 100, height: 300 }, + deepestSize + ]; + for (let i = 0; i < elements.length; i++) { + assert.deepEqual(elements[i].size(), expectedSizes[i]); + } }); - QUnit.test('should resize clusters', function(assert) { + QUnit.test('resizeClusters: true, clusterPadding: number - should resize clusters according to our algorithm', function(assert) { const deepestSize = { width: 500, @@ -345,9 +382,9 @@ QUnit.module('DirectedGraph', function(hooks) { clusterPadding: padding }); + // Parents are resized to fit all children + // - note that we are checking from deepest child up const nextExpectedSize = deepestSize; - - // Parents should be resized to fit all children for (let i = elements.length - 1; i >= 0; i--) { assert.deepEqual(elements[i].size(), nextExpectedSize); nextExpectedSize.width += padding * 2; @@ -355,7 +392,7 @@ QUnit.module('DirectedGraph', function(hooks) { } }); - QUnit.test('should not resize clusters if `glGraph` does not hold reference to their children', function(assert) { + QUnit.test('resizeClusters: true, clusterPadding: number - should not resize clusters if `glGraph` does not hold reference to their children', function(assert) { const containerSize = { width: 500, @@ -374,11 +411,148 @@ QUnit.module('DirectedGraph', function(hooks) { graph.resetCells([container1, container2, rect1, rect2]); // Do not pass the children to the layout function + // opt.clusterPadding = `10` by default DirectedGraph.layout([container1, container2], { resizeClusters: true }); - // Size remains unchanged + // Sizes remain unchanged + assert.deepEqual(container1.size(), containerSize); + assert.deepEqual(container2.size(), containerSize); + }); + + QUnit.test('resizeClusters: false, clusterPadding: \'default\' - should not resize clusters', function(assert) { + + const deepestSize = { + width: 500, + height: 500 + }; + + const elements = [ + new joint.shapes.standard.Rectangle({ size: { width: 60, height: 60 }}), + new joint.shapes.standard.Rectangle({ size: { width: 120, height: 120 }}), + new joint.shapes.standard.Rectangle({ size: { width: 100, height: 300 }}), + new joint.shapes.standard.Rectangle({ size: deepestSize }) + ]; + + elements[0].embed(elements[1]); + elements[1].embed(elements[2]); + elements[2].embed(elements[3]); + + graph.resetCells(elements); + + DirectedGraph.layout(graph, { + resizeClusters: false, + clusterPadding: 'default' + }); + + // Sizes remain unchanged + const expectedSizes = [ + { width: 60, height: 60 }, + { width: 120, height: 120 }, + { width: 100, height: 300 }, + deepestSize + ]; + for (let i = 0; i < elements.length; i++) { + assert.deepEqual(elements[i].size(), expectedSizes[i]); + } + }); + + QUnit.test('resizeClusters: true, clusterPadding: \'default\' - should resize nested clusters according to default dagre algorithm', function(assert) { + + const deepestSize = { + width: 500, + height: 500 + }; + + const elements = [ + new joint.shapes.standard.Rectangle({ size: { width: 60, height: 60 }}), + new joint.shapes.standard.Rectangle({ size: { width: 120, height: 120 }}), + new joint.shapes.standard.Rectangle({ size: { width: 100, height: 300 }}), + new joint.shapes.standard.Rectangle({ size: deepestSize }) + ]; + + elements[0].embed(elements[1]); + elements[1].embed(elements[2]); + elements[2].embed(elements[3]); + + graph.resetCells(elements); + + // opt.resizeClusters = `true` by default + DirectedGraph.layout(graph, { + clusterPadding: 'default' + }); + + const expectedSizes = [ + { width: 650, height: 650 }, + { width: 610, height: 600 }, + { width: 570, height: 550 }, + deepestSize + ]; + for (let i = 0; i < elements.length; i++) { + assert.deepEqual(elements[i].size(), expectedSizes[i]); + } + }); + + QUnit.test('resizeClusters: true, clusterPadding: \'default\' - should resize clusters with connected non-embedded children according to default dagre algorithm', function(assert) { + + const elements = [ + new joint.shapes.standard.Rectangle({ position: { x: 0, y: 0 }, size: { width: 30, height: 30 }}), + new joint.shapes.standard.Rectangle({ position: { x: 100, y: 100 }, size: { width: 30, height: 30 }}), + new joint.shapes.standard.Rectangle({ position: { x: 100, y: 200 }, size: { width: 30, height: 30 }}), + new joint.shapes.standard.Rectangle({ position: { x: 200, y: 200 }, size: { width: 30, height: 30 }}), + ]; + + const links = [ + new joint.shapes.standard.Link({ source: elements[1], target: elements[2] }), + new joint.shapes.standard.Link({ source: elements[1], target: elements[3] }) + ]; + + elements[0].embed(elements[1]); + + graph.resetCells([...elements, ...links]); + + // opt.resizeClusters = `true` by default + DirectedGraph.layout(graph, { + clusterPadding: 'default' + }); + + const expectedBBoxes = [ + { x: 0, y: 0, width: 160, height: 80 }, + { x: 70, y: 25, width: 30, height: 30 }, + { x: 30, y: 205, width: 30, height: 30 }, + { x: 110, y: 205, width: 30, height: 30 } + ]; + for (let i = 0; i < elements.length; i++) { + assert.deepEqual(elements[i].getBBox(), new joint.g.Rect(expectedBBoxes[i])); + } + }); + + QUnit.test('resizeClusters: true, clusterPadding: \'default\' - should not resize clusters if `glGraph` does not hold reference to their children', function(assert) { + + const containerSize = { + width: 500, + height: 500 + }; + + const container1 = new joint.shapes.standard.Rectangle({ size: containerSize }); + const container2 = new joint.shapes.standard.Rectangle({ size: containerSize }); + + const rect1 = new joint.shapes.standard.Rectangle({ size: { width: 60, height: 60 }}); + const rect2 = new joint.shapes.standard.Rectangle({ size: { width: 120, height: 120 }}); + + container1.embed(rect1); + container2.embed(rect2); + + graph.resetCells([container1, container2, rect1, rect2]); + + // Do not pass the children to the layout function + // opt.resizeClusters = `true` by default + DirectedGraph.layout([container1, container2], { + clusterPadding: 'default' + }); + + // Sizes remain unchanged assert.deepEqual(container1.size(), containerSize); assert.deepEqual(container2.size(), containerSize); });