From a887212146f39bf60e7e705b124c001cb74f1fc1 Mon Sep 17 00:00:00 2001 From: zbynekstara Date: Mon, 14 Apr 2025 11:03:54 +0200 Subject: [PATCH 1/8] feat(layout-directed-graph): support clusterPadding: 'default' --- .../joint-layout-directed-graph/DirectedGraph.mjs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/joint-layout-directed-graph/DirectedGraph.mjs b/packages/joint-layout-directed-graph/DirectedGraph.mjs index f8fe162628..2e3a85bda0 100644 --- a/packages/joint-layout-directed-graph/DirectedGraph.mjs +++ b/packages/joint-layout-directed-graph/DirectedGraph.mjs @@ -116,6 +116,20 @@ export const DirectedGraph = { exportLink: this.exportLink }); + if (opt.clusterPadding === 'default') { + opt.resizeClusters = false; + // use default dagre approach + opt.setPosition = (e, position) => { + if (e.getEmbeddedCells().length > 0) { + e.position(position.x - position.width / 2, position.y - position.height / 2); + e.size(position.width, position.height); + } else { + const size = e.size(); + e.position(position.x - size.width / 2, position.y - size.height / 2); + } + } + } + // create a graphlib.Graph that represents the joint.dia.Graph // var glGraph = graph.toGraphLib({ var glGraph = DirectedGraph.toGraphLib(graph, { From 22b95069e672e1ece27cdbd0f34023852bece0e2 Mon Sep 17 00:00:00 2001 From: zbynekstara Date: Mon, 14 Apr 2025 15:43:26 +0200 Subject: [PATCH 2/8] do not overwrite resizeClusters option --- packages/joint-layout-directed-graph/DirectedGraph.mjs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/joint-layout-directed-graph/DirectedGraph.mjs b/packages/joint-layout-directed-graph/DirectedGraph.mjs index 2e3a85bda0..4b70cb8104 100644 --- a/packages/joint-layout-directed-graph/DirectedGraph.mjs +++ b/packages/joint-layout-directed-graph/DirectedGraph.mjs @@ -117,7 +117,6 @@ export const DirectedGraph = { }); if (opt.clusterPadding === 'default') { - opt.resizeClusters = false; // use default dagre approach opt.setPosition = (e, position) => { if (e.getEmbeddedCells().length > 0) { @@ -186,7 +185,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() From 57d6498c3a525bc27d6aa2ee8a214da1c71cbf51 Mon Sep 17 00:00:00 2001 From: zbynekstara Date: Mon, 14 Apr 2025 16:17:30 +0200 Subject: [PATCH 3/8] move logic to importElement() function --- .../DirectedGraph.mjs | 28 ++++++------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/packages/joint-layout-directed-graph/DirectedGraph.mjs b/packages/joint-layout-directed-graph/DirectedGraph.mjs index 4b70cb8104..188075edf2 100644 --- a/packages/joint-layout-directed-graph/DirectedGraph.mjs +++ b/packages/joint-layout-directed-graph/DirectedGraph.mjs @@ -36,16 +36,17 @@ 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', { - x: glNode.x - glNode.width / 2, - y: glNode.y - glNode.height / 2 - }); + element.position(glNode.x - glNode.width / 2, glNode.y - glNode.height / 2); + if ((opt.clusterPadding === 'default') && (element.getEmbeddedCells().length > 0)) { + // apply cluster padding according to Dagre's calculation + element.size(glNode.width, glNode.height); + } // else: rely on `opt.resizeClusters` and `opt.clusterPadding` (see `layout()` function) } }, @@ -116,19 +117,6 @@ export const DirectedGraph = { exportLink: this.exportLink }); - if (opt.clusterPadding === 'default') { - // use default dagre approach - opt.setPosition = (e, position) => { - if (e.getEmbeddedCells().length > 0) { - e.position(position.x - position.width / 2, position.y - position.height / 2); - e.size(position.width, position.height); - } else { - const size = e.size(); - e.position(position.x - size.width / 2, position.y - size.height / 2); - } - } - } - // create a graphlib.Graph that represents the joint.dia.Graph // var glGraph = graph.toGraphLib({ var glGraph = DirectedGraph.toGraphLib(graph, { From d85ae65322af0bdb39f141331a07c33345e38cad Mon Sep 17 00:00:00 2001 From: zbynekstara Date: Mon, 14 Apr 2025 17:09:49 +0200 Subject: [PATCH 4/8] use glNode.rank === undefined to see if something is a cluster --- packages/joint-layout-directed-graph/DirectedGraph.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/joint-layout-directed-graph/DirectedGraph.mjs b/packages/joint-layout-directed-graph/DirectedGraph.mjs index 188075edf2..085e2612b1 100644 --- a/packages/joint-layout-directed-graph/DirectedGraph.mjs +++ b/packages/joint-layout-directed-graph/DirectedGraph.mjs @@ -43,8 +43,8 @@ export const DirectedGraph = { opt.setPosition(element, glNode); } else { element.position(glNode.x - glNode.width / 2, glNode.y - glNode.height / 2); - if ((opt.clusterPadding === 'default') && (element.getEmbeddedCells().length > 0)) { - // apply cluster padding according to Dagre's calculation + if ((opt.clusterPadding === 'default') && (glNode.rank === undefined)) { + // we want to use Dagre's default cluster padding, and this is a cluster element.size(glNode.width, glNode.height); } // else: rely on `opt.resizeClusters` and `opt.clusterPadding` (see `layout()` function) } From f0042da419562ad0a5c5161d8f1926200a5ad81e Mon Sep 17 00:00:00 2001 From: zbynekstara Date: Fri, 25 Apr 2025 11:36:23 +0200 Subject: [PATCH 5/8] tests --- .../DirectedGraph.mjs | 10 +- .../joint-layout-directed-graph/test/index.js | 186 +++++++++++++++++- 2 files changed, 186 insertions(+), 10 deletions(-) diff --git a/packages/joint-layout-directed-graph/DirectedGraph.mjs b/packages/joint-layout-directed-graph/DirectedGraph.mjs index 085e2612b1..b121a0055f 100644 --- a/packages/joint-layout-directed-graph/DirectedGraph.mjs +++ b/packages/joint-layout-directed-graph/DirectedGraph.mjs @@ -43,11 +43,13 @@ export const DirectedGraph = { opt.setPosition(element, glNode); } else { element.position(glNode.x - glNode.width / 2, glNode.y - glNode.height / 2); - if ((opt.clusterPadding === 'default') && (glNode.rank === undefined)) { - // we want to use Dagre's default cluster padding, and this is a cluster - element.size(glNode.width, glNode.height); - } // else: rely on `opt.resizeClusters` and `opt.clusterPadding` (see `layout()` function) } + + // check if we want to use Dagre's default cluster padding + if (opt.resizeClusters && (opt.clusterPadding === 'default') && (glNode.rank === undefined)) { + // when `glNode.rank === undefined`, it means that the current element is a cluster + element.size(glNode.width, glNode.height); + } // else: possibly apply numeric `opt.clusterPadding` (see `layout()` function) }, importLink: function(edgeObj, glGraph, graph, opt) { 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); }); From a27c856dd785833efe09788f7dda91dea11c4c3f Mon Sep 17 00:00:00 2001 From: zbynekstara Date: Fri, 25 Apr 2025 11:47:08 +0200 Subject: [PATCH 6/8] use set() --- packages/joint-layout-directed-graph/DirectedGraph.mjs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/joint-layout-directed-graph/DirectedGraph.mjs b/packages/joint-layout-directed-graph/DirectedGraph.mjs index b121a0055f..02f422d63b 100644 --- a/packages/joint-layout-directed-graph/DirectedGraph.mjs +++ b/packages/joint-layout-directed-graph/DirectedGraph.mjs @@ -42,13 +42,19 @@ export const DirectedGraph = { if (util.isFunction(opt.setPosition)) { opt.setPosition(element, glNode); } else { - element.position(glNode.x - glNode.width / 2, glNode.y - glNode.height / 2); + element.set('position', { + x: glNode.x - glNode.width / 2, + y: glNode.y - glNode.height / 2 + }); } // check if we want to use Dagre's default cluster padding if (opt.resizeClusters && (opt.clusterPadding === 'default') && (glNode.rank === undefined)) { // when `glNode.rank === undefined`, it means that the current element is a cluster - element.size(glNode.width, glNode.height); + element.set('size', { + width: glNode.width, + height: glNode.height + }); } // else: possibly apply numeric `opt.clusterPadding` (see `layout()` function) }, From 963720856566b699277d8941429ed353b34046cd Mon Sep 17 00:00:00 2001 From: zbynekstara Date: Fri, 25 Apr 2025 18:38:25 +0200 Subject: [PATCH 7/8] types --- .../joint-layout-directed-graph/DirectedGraph.d.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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; } From cdb757fed0ecc875423960bc54556ec3b7277c54 Mon Sep 17 00:00:00 2001 From: Roman Bruckner Date: Sat, 26 Apr 2025 13:26:07 +0800 Subject: [PATCH 8/8] Update DirectedGraph.mjs --- .../joint-layout-directed-graph/DirectedGraph.mjs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/joint-layout-directed-graph/DirectedGraph.mjs b/packages/joint-layout-directed-graph/DirectedGraph.mjs index 02f422d63b..9a33bf4f54 100644 --- a/packages/joint-layout-directed-graph/DirectedGraph.mjs +++ b/packages/joint-layout-directed-graph/DirectedGraph.mjs @@ -49,12 +49,14 @@ export const DirectedGraph = { } // check if we want to use Dagre's default cluster padding - if (opt.resizeClusters && (opt.clusterPadding === 'default') && (glNode.rank === undefined)) { - // when `glNode.rank === undefined`, it means that the current element is a cluster - element.set('size', { - width: glNode.width, - height: glNode.height - }); + 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) },