Skip to content

fix(layout.DirectedGraph): better error message when dagre fails to connect to container #2838

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions packages/joint-layout-directed-graph/DirectedGraph.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export namespace DirectedGraph {
rankSep?: number;
marginX?: number;
marginY?: number;
validateGraph?: boolean;
resizeClusters?: boolean;
clusterPadding?: dia.Padding | 'default';
debugTiming?: boolean;
Expand Down
32 changes: 29 additions & 3 deletions packages/joint-layout-directed-graph/DirectedGraph.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,29 @@ export const DirectedGraph = {
}
},

/**
* Validates that the graph does not contain an arrangement of cells that Dagre considers invalid:
* - A child connected to a container.
* - A container connected to a child.
* - A container connected to a container.
* @param {dia.Graph} graph The graph to validate.
* @throws {Error} Throws an error if an invalid arrangement is encountered.
*/
validateGraph: function(graph) {

graph.getLinks().forEach((link) => {
const source = link.getSourceElement();
const target = link.getTargetElement();
// is container = is element && has at least one embedded element
const isSourceContainer = source && source.getEmbeddedCells().some((cell) => cell.isElement());
const isTargetContainer = target && target.getEmbeddedCells().some((cell) => cell.isElement());
if ((isSourceContainer && target) || (source && isTargetContainer)) {
// see https://github.com/clientIO/joint/issues/455
throw new Error('DirectedGraph: It is not possible to connect a child to a container.');
}
});
},

layout: function(graphOrCells, opt) {

var graph;
Expand All @@ -121,15 +144,18 @@ export const DirectedGraph = {
graphOrCells = null;

opt = util.defaults(opt || {}, {
validateGraph: true,
resizeClusters: true,
clusterPadding: 10,
exportElement: this.exportElement,
exportLink: this.exportLink,
disableOptimalOrderHeuristic: false
});

// create a graphlib.Graph that represents the joint.dia.Graph
// var glGraph = graph.toGraphLib({
// Check that we are not trying to connect a child to a container.
if (opt.validateGraph) this.validateGraph(graph);

// Create a graphlib.Graph that represents the joint.dia.Graph
var glGraph = DirectedGraph.toGraphLib(graph, {
directed: true,
// We are about to use edge naming feature.
Expand Down Expand Up @@ -179,7 +205,7 @@ export const DirectedGraph = {
}
}

// Executes the layout.
// Execute the layout.
dagreUtil.layout(glGraph, {
debugTiming: !!opt.debugTiming,
disableOptimalOrderHeuristic: !!opt.disableOptimalOrderHeuristic,
Expand Down
70 changes: 70 additions & 0 deletions packages/joint-layout-directed-graph/test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -583,5 +583,75 @@ QUnit.module('DirectedGraph', function(hooks) {
}
});
});

QUnit.test('should throw an understandable error when trying to connect a child to a container', function(assert) {
const elements = [
new joint.shapes.standard.Rectangle({ position: { x: 50, y: 50 }, size: { width: 300, height: 300 } }),
new joint.shapes.standard.Rectangle({ position: { x: 175, y: 175 }, size: {width: 50, height: 50 } }),
new joint.shapes.standard.Rectangle({ position: { x: 400, y: 50 }, size: { width: 300, height: 300 } }),
new joint.shapes.standard.Rectangle({ position: { x: 525, y: 175 }, size: { width: 50, height: 50 } }),
];

elements[0].embed(elements[1]);
elements[2].embed(elements[3]);

const invalidGraphLinks = [
// this throws error:
new joint.shapes.standard.Link({ source: { id: elements[1].id }, target: { id: elements[0].id }}), // child -> its container
new joint.shapes.standard.Link({ source: { id: elements[1].id }, target: { id: elements[2].id }}), // child -> unrelated container
new joint.shapes.standard.Link({ source: { id: elements[0].id }, target: { id: elements[1].id }}), // container -> its child
new joint.shapes.standard.Link({ source: { id: elements[0].id }, target: { id: elements[3].id }}), // container -> unrelated child
new joint.shapes.standard.Link({ source: { id: elements[0].id }, target: { id: elements[2].id }}), // container -> unrelated container
];
const validGraphLinks = [
// this is ok:
new joint.shapes.standard.Link({ source: { id: elements[1].id }, target: { id: elements[3].id }}), // child -> unrelated child
new joint.shapes.standard.Link({ source: { id: elements[1].id }, target: { x: 0, y: 0 }}), // child -> point
new joint.shapes.standard.Link({ source: { x: 0, y: 0 }, target: { id: elements[1].id }}), // point -> child
new joint.shapes.standard.Link({ source: { id: elements[0].id }, target: { x: 0, y: 0 }}), // container -> point
new joint.shapes.standard.Link({ source: { x: 0, y: 0 }, target: { id: elements[0].id }}), // point -> container
];

let cells;

// Using `validateGraph` option (default):
invalidGraphLinks.forEach((link) => {
cells = elements.concat([link]);
graph.resetCells(cells);
assert.throws(() => {
DirectedGraph.layout(graph);
},
new Error('DirectedGraph: It is not possible to connect a child to a container.'),
'Should throw our own Error about an attempt having been made to connect a child to a container'
);
});
validGraphLinks.forEach((link) => {
cells = elements.concat([link]);
graph.resetCells(cells);
assert.ok(DirectedGraph.layout(graph) instanceof g.Rect);
});

// Disabling `validateGraph` option:
invalidGraphLinks.forEach((link) => {
cells = elements.concat([link]);
graph.resetCells(cells);
assert.throws(() => {
DirectedGraph.layout(graph, { validateGraph: false });
},
(err) => {
return err instanceof TypeError &&
err.message.includes('set') &&
err.message.includes('rank') &&
err.message.includes('undefined');
},
'Should cause a JavaScript TypeError about not being able to set `rank` on `undefined`'
);
});
validGraphLinks.forEach((link) => {
cells = elements.concat([link]);
graph.resetCells(cells);
assert.ok(DirectedGraph.layout(graph, { validateGraph: false }) instanceof g.Rect);
});
})
});
});
Loading