From 5704e9c8ff237cb9158012736a06827642e8a69e Mon Sep 17 00:00:00 2001 From: Michael Behrisch <m.behrisch@uu.nl> Date: Mon, 14 Mar 2022 22:00:04 +0100 Subject: [PATCH] fix(libs): :zap: cytoscape layouts take width and height into consideration The cytoscape layouts needed the headless mode to compute the width and height correctly. Now the layouts are working with these (computed) options. --- .vscode/settings.json | 3 +- .../src/components/schema/schema.tsx | 9 +- .../data-access/store/src/lib/schemaSlice.ts | 2 +- .../graph-layout/src/lib/cytoscape-layouts.ts | 262 +++++++++--------- 4 files changed, 139 insertions(+), 137 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 84265c462..6324a78d9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -21,7 +21,8 @@ "vis-paoh", "vis-schema", "storybook", - "store" + "store", + "libs" ], "jest.jestCommandLine": "nx affected:test" } diff --git a/apps/web-graphpolaris/src/components/schema/schema.tsx b/apps/web-graphpolaris/src/components/schema/schema.tsx index b8b334a89..4167487ad 100644 --- a/apps/web-graphpolaris/src/components/schema/schema.tsx +++ b/apps/web-graphpolaris/src/components/schema/schema.tsx @@ -36,14 +36,13 @@ const Schema = (props: Props) => { console.log('schema Layout', schemaLayout, 'order', dbschema.order); const layout = layoutFactory.createLayout( schemaLayout as AllLayoutAlgorithms - // schemaLayout ); layout?.layout(dbschema); - dbschema.forEachNode((node, attr) => { - console.log('x', dbschema.getNodeAttribute(node, 'x')); - console.log('y', dbschema.getNodeAttribute(node, 'y')); - }); + // dbschema.forEachNode((node, attr) => { + // console.log('x', dbschema.getNodeAttribute(node, 'x')); + // console.log('y', dbschema.getNodeAttribute(node, 'y')); + // }); const flowElements = createReactFlowElements(dbschema); setElements(flowElements); diff --git a/libs/shared/data-access/store/src/lib/schemaSlice.ts b/libs/shared/data-access/store/src/lib/schemaSlice.ts index 1769c7b90..9aabb7ca1 100644 --- a/libs/shared/data-access/store/src/lib/schemaSlice.ts +++ b/libs/shared/data-access/store/src/lib/schemaSlice.ts @@ -9,7 +9,7 @@ import type { RootState } from './store'; // Define the initial state using that type export const initialState = { graphologySerialized: new MultiGraph().export(), - layoutName: 'Cytoscape_klay', + layoutName: 'Cytoscape_fcose', }; export const schemaSlice = createSlice({ diff --git a/libs/shared/graph-layout/src/lib/cytoscape-layouts.ts b/libs/shared/graph-layout/src/lib/cytoscape-layouts.ts index 9dcb62fdc..b5190896b 100644 --- a/libs/shared/graph-layout/src/lib/cytoscape-layouts.ts +++ b/libs/shared/graph-layout/src/lib/cytoscape-layouts.ts @@ -33,13 +33,16 @@ export type CytoscapeLayoutAlgorithms = type CytoNode = { data: { id: string; - type: string; + type?: string; source?: string; target?: string; position?: { x: number; y: number; }; + label?: string; + count?: number; + color?: string; }; }; /** @@ -78,6 +81,8 @@ export class CytoscapeFactory } export abstract class Cytoscape extends Layout<CytoscapeProvider> { + public verbose = false; + constructor(public override algorithm: LayoutAlgorithm<CytoscapeProvider>) { super('Cytoscape', algorithm); } @@ -92,18 +97,125 @@ export abstract class Cytoscape extends Layout<CytoscapeProvider> { graph.forEachNode((node) => { cytonodes.push({ - data: { id: node, type: 'node' }, + data: { + id: node, + // type: 'node', + label: 'start', + count: 50, + color: 'green', + }, }); }); graph.forEachEdge((edge, _attributes, source, target) => { cytonodes.push({ - data: { id: edge, type: 'edge', source: source, target: target }, + data: { + id: edge, + // type: 'edge', + source: source, + target: target, + }, }); }); return cytonodes; } + + public setVerbose(verbose: boolean) { + this.verbose = verbose; + } + + makeCytoscapeInstance(cytonodes: CytoNode[]) { + return cytoscape({ + elements: cytonodes, + + headless: true, + styleEnabled: true, + + style: [ + // the stylesheet for the graph + { + selector: 'node', + style: { + label: 'data(id)', + width: (node: any) => { + const ctx = document.createElement('canvas').getContext('2d'); + const fStyle = node.pstyle('font-style').strValue; + const size = node.pstyle('font-size').pfValue + 'px'; + const family = node.pstyle('font-family').strValue; + const weight = node.pstyle('font-weight').strValue; + + // const padding = node.pstyle('padding-left').; + + ctx!.font = fStyle + ' ' + weight + ' ' + size + ' ' + family; + return ctx!.measureText(node.data('id')).width; + }, + height: '15px', + shape: 'round-rectangle', + 'background-color': '#fff', + 'text-valign': 'center', + color: '#333333', + 'border-width': 1, + 'border-color': '#2E1A61', + 'font-size': '10px', + 'padding-left': '30px', + 'padding-right': '30px', + 'padding-top': '50px', + 'padding-bottom': '50px', + }, + // selector: 'node', + // style: { + // content: 'data(id)', + // // label: 'data(id)', + // shape: 'round-rectangle', + // // width: 'label', + // // height: 200, + // 'font-size': '10px', + // 'padding-left': '5px', + // 'padding-right': '5px', + // 'padding-top': '5px', + // 'padding-bottom': '5px', + // }, + }, + + { + selector: 'edge', + style: { + 'line-color': '#ccc', + 'target-arrow-color': '#ccc', + 'target-arrow-shape': 'triangle', + 'curve-style': 'bezier', + }, + }, + ], + }); + } + + updateNodePositions(nodes: cytoscape.NodeCollection, graph: Graph) { + nodes.forEach((node: cytoscape.NodeSingular) => { + if (this.verbose) { + console.log(node.id(), node.position()); + } + + // graph.setNodeAttribute(node.id(), 'x', node.position().x); + // graph.setNodeAttribute(node.id(), 'y', node.position().y); + + graph.updateNode(node.id(), (attr) => { + return { + ...attr, + x: node.position().x, + y: node.position().y, + }; + }); + + console.log( + 'width', + node.numericStyle('width'), + 'height', + node.numericStyle('height') + ); + }); + } } /** @@ -115,50 +227,19 @@ class CytoscapeKlay extends Cytoscape { } public override layout( - graph: Graph<Attributes, Attributes, Attributes>, - verbose?: boolean + graph: Graph<Attributes, Attributes, Attributes> ): void { const cytonodes: CytoNode[] = this.convertToCytoscapeModel(graph); cytoscape.use(klay); - const cy = cytoscape({ - elements: cytonodes, - // style: [ - // { - // selector: 'node', - // style: { - // 'shape': 'round-rectangle', - // 'background-color': '#fff', - // 'label': 'data(id)', - // 'text-valign': 'center', - // 'color': '#333333', - // 'border-width': 1, - // 'border-color': '#2E1A61', - // 'width': (node: any) => { - // const ctx = document.createElement('canvas').getContext('2d'); - // const fStyle = node.pstyle('font-style').strValue; - // const size = node.pstyle('font-size').pfValue + 'px'; - // const family = node.pstyle('font-family').strValue; - // const weight = node.pstyle('font-weight').strValue; - - // ctx!.font = fStyle + ' ' + weight + ' ' + size + ' ' + family; - // return ctx!.measureText(node.data('name')); - // }, - // 'font-size': '10px', - // 'padding-left': '5px', - // 'padding-right': '5px', - // 'padding-top': '5px', - // 'padding-bottom': '5px', - - // } - // } - // ] - }); + const cy = this.makeCytoscapeInstance(cytonodes); const layout = cy.layout({ name: 'klay', + nodeDimensionsIncludeLabels: true, + // Constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h } // boundingBox: undefined, @@ -168,30 +249,11 @@ class CytoscapeKlay extends Cytoscape { stop: function () { console.log('Layout.stop'); }, // on layoutstop - }); + } as any); layout.run(); const nodes = cy.nodes(); - nodes.forEach((node) => { - if (verbose) { - console.log(node.id(), node.position()); - } - // console.log( - // node.id(), - // node.position(), - // node.boundingBox({ - // includeLabels: true, - // }), - // node.renderedBoundingbox({ - // includeLabels: true, - // }) - // ); - - graph.setNodeAttribute(node.id(), 'x', node.position().x); - graph.setNodeAttribute(node.id(), 'y', node.position().y); - }); - - // console.log(nodes); + this.updateNodePositions(nodes, graph); // var options = { // nodeDimensionsIncludeLabels: false, // Boolean which changes whether label dimensions are included when calculating node dimensions @@ -292,9 +354,7 @@ class CytoscapeElk extends Cytoscape { ): void { const cytonodes: CytoNode[] = this.convertToCytoscapeModel(graph); - const cy = cytoscape({ - elements: cytonodes, - }); + const cy = this.makeCytoscapeInstance(cytonodes); // const options = { // name: 'elk', @@ -365,9 +425,7 @@ class CytoscapeElk extends Cytoscape { name: 'elk', fit: true, ranker: 'longest-path', - animate: true, - animationDuration: 300, - animationEasing: 'ease-in-out-cubic', + animate: false, elk: { zoomToFit: true, algorithm: 'mrtree', @@ -382,25 +440,7 @@ class CytoscapeElk extends Cytoscape { } as any); layout.run(); - const nodes = cy.nodes(); - nodes.forEach((node) => { - if (verbose) { - console.log(node.id(), node.position()); - } - // console.log( - // node.id(), - // node.position() - // // node.boundingBox({ - // // includeLabels: true, - // // }), - // // node.renderedBoundingbox({ - // // includeLabels: true, - // // }) - // ); - - graph.setNodeAttribute(node.id(), 'x', node.position().x); - graph.setNodeAttribute(node.id(), 'y', node.position().y); - }); + this.updateNodePositions(cy.nodes(), graph); } } @@ -419,9 +459,7 @@ class CytoscapeDagre extends Cytoscape { ): void { const cytonodes: CytoNode[] = this.convertToCytoscapeModel(graph); - const cy = cytoscape({ - elements: cytonodes, - }); + const cy = this.makeCytoscapeInstance(cytonodes); const layout = cy.layout({ name: 'dagre', @@ -434,14 +472,7 @@ class CytoscapeDagre extends Cytoscape { } as any); layout.run(); - const nodes = cy.nodes(); - nodes.forEach((node) => { - if (verbose) { - console.log(node.id(), node.position()); - } - graph.setNodeAttribute(node.id(), 'x', node.position().x); - graph.setNodeAttribute(node.id(), 'y', node.position().y); - }); + this.updateNodePositions(cy.nodes(), graph); } } @@ -460,9 +491,7 @@ class CytoscapeFCose extends Cytoscape { ): void { const cytonodes: CytoNode[] = this.convertToCytoscapeModel(graph); - const cy = cytoscape({ - elements: cytonodes, - }); + const cy = this.makeCytoscapeInstance(cytonodes); const layout = cy.layout({ name: 'fcose', @@ -477,14 +506,7 @@ class CytoscapeFCose extends Cytoscape { layout.run(); const nodes = cy.nodes(); - nodes.forEach((node) => { - if (verbose) { - console.log(node.id(), node.position()); - } - - graph.setNodeAttribute(node.id(), 'x', node.position().x); - graph.setNodeAttribute(node.id(), 'y', node.position().y); - }); + this.updateNodePositions(nodes, graph); } } @@ -503,9 +525,7 @@ class CytoscapeCoseBilkent extends Cytoscape { ): void { const cytonodes: CytoNode[] = this.convertToCytoscapeModel(graph); - const cy = cytoscape({ - elements: cytonodes, - }); + const cy = this.makeCytoscapeInstance(cytonodes); const layout = cy.layout({ name: 'cose-bilkent', @@ -519,15 +539,7 @@ class CytoscapeCoseBilkent extends Cytoscape { } as any); layout.run(); - const nodes = cy.nodes(); - nodes.forEach((node) => { - if (verbose) { - console.log(node.id(), node.position()); - } - - graph.setNodeAttribute(node.id(), 'x', node.position().x); - graph.setNodeAttribute(node.id(), 'y', node.position().y); - }); + this.updateNodePositions(cy.nodes(), graph); } } @@ -546,9 +558,7 @@ class CytoscapeCise extends Cytoscape { ): void { const cytonodes: CytoNode[] = this.convertToCytoscapeModel(graph); - const cy = cytoscape({ - elements: cytonodes, - }); + const cy = this.makeCytoscapeInstance(cytonodes); const layout = cy.layout({ name: 'cise', @@ -561,14 +571,6 @@ class CytoscapeCise extends Cytoscape { } as any); layout.run(); - const nodes = cy.nodes(); - nodes.forEach((node) => { - if (verbose) { - console.log(node.id(), node.position()); - } - - graph.setNodeAttribute(node.id(), 'x', node.position().x); - graph.setNodeAttribute(node.id(), 'y', node.position().y); - }); + this.updateNodePositions(cy.nodes(), graph); } } -- GitLab