From 2c26f4edf45df0c2936a535a19b799d524cf5064 Mon Sep 17 00:00:00 2001 From: Michael Behrisch <m.behrisch@uu.nl> Date: Tue, 15 Feb 2022 17:04:29 +0100 Subject: [PATCH] feat(vis-schema): :sparkles: introduces graph-layout library with various algorithms This feature introduces @graphpolaris/shared/graph-layout with four layouts (circular, noverlap, random, forceatlas2) Shows how to apply layouts to graphology graphs in the schema. --- .vscode/launch.json | 22 +- apps/web-graphpolaris/.babelrc | 4 +- apps/web-graphpolaris/src/app/app.tsx | 5 +- .../src/components/schema/initial-elements.js | 121 ++++++++++ .../src/components/schema/schema.stories.tsx | 136 ++--------- .../src/components/schema/schema.tsx | 85 ++++--- .../src/lib/schema-usecases.spec.ts | 62 ++++- .../src/lib/schema-usecases.ts | 7 +- .../shared/data-access/store/src/lib/hooks.ts | 12 +- .../store/src/lib/schemaSlice.spec.ts | 64 ++---- .../data-access/store/src/lib/schemaSlice.ts | 31 ++- libs/shared/graph-layout/.babelrc | 3 + libs/shared/graph-layout/.eslintrc.json | 18 ++ libs/shared/graph-layout/README.md | 7 + libs/shared/graph-layout/jest.config.js | 15 ++ libs/shared/graph-layout/project.json | 29 +++ .../shared/graph-layout/src/factory/Layout.ts | 21 ++ .../src/factory/cytoscape-layouts.ts | 54 +++++ .../src/factory/graphology-layouts.ts | 169 ++++++++++++++ .../factory/layout-creator-usecase.spec.ts | 142 ++++++++++++ .../src/factory/layout-creator-usecase.ts | 77 +++++++ libs/shared/graph-layout/src/factory/test.ts | 0 libs/shared/graph-layout/src/index.ts | 6 + libs/shared/graph-layout/tsconfig.json | 22 ++ libs/shared/graph-layout/tsconfig.lib.json | 11 + libs/shared/graph-layout/tsconfig.spec.json | 19 ++ package.json | 3 + tsconfig.base.json | 4 + yarn.lock | 212 +++++++++++++++++- 29 files changed, 1146 insertions(+), 215 deletions(-) create mode 100644 apps/web-graphpolaris/src/components/schema/initial-elements.js create mode 100644 libs/shared/graph-layout/.babelrc create mode 100644 libs/shared/graph-layout/.eslintrc.json create mode 100644 libs/shared/graph-layout/README.md create mode 100644 libs/shared/graph-layout/jest.config.js create mode 100644 libs/shared/graph-layout/project.json create mode 100644 libs/shared/graph-layout/src/factory/Layout.ts create mode 100644 libs/shared/graph-layout/src/factory/cytoscape-layouts.ts create mode 100644 libs/shared/graph-layout/src/factory/graphology-layouts.ts create mode 100644 libs/shared/graph-layout/src/factory/layout-creator-usecase.spec.ts create mode 100644 libs/shared/graph-layout/src/factory/layout-creator-usecase.ts create mode 100644 libs/shared/graph-layout/src/factory/test.ts create mode 100644 libs/shared/graph-layout/src/index.ts create mode 100644 libs/shared/graph-layout/tsconfig.json create mode 100644 libs/shared/graph-layout/tsconfig.lib.json create mode 100644 libs/shared/graph-layout/tsconfig.spec.json diff --git a/.vscode/launch.json b/.vscode/launch.json index f73ba1c65..520ee6d53 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -14,12 +14,22 @@ "port": 9229 }, { - // Requires the extension Debugger for Chrome: https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome + "name": "Run Storybook server", + "type": "node", + "request": "launch", + "protocol": "inspector", + "program": "${workspaceRoot}/node_modules/.bin/start-storybook", + "args": ["-p", "6006"], + "stopOnEntry": false, + "runtimeArgs": ["--nolazy"], + "sourceMaps": false + }, + { "type": "chrome", "request": "launch", - "name": "Storybook Debug", + "name": "Launch Chrome for Storybook", "breakOnLoad": true, - "url": "http://localhost:4400/?path=/story/", + "url": "http://localhost:6006", "sourceMaps": true, "webRoot": "${workspaceFolder}", "sourceMapPathOverrides": { @@ -29,5 +39,11 @@ "webpack:///./~/*": "${webRoot}/node_modules/*" } } + ], + "compounds": [ + { + "name": "Launch Storybook", + "configurations": ["Launch Chrome for Storybook", "Run Storybook server"] + } ] } diff --git a/apps/web-graphpolaris/.babelrc b/apps/web-graphpolaris/.babelrc index 61641ec8a..09947a5fd 100644 --- a/apps/web-graphpolaris/.babelrc +++ b/apps/web-graphpolaris/.babelrc @@ -5,7 +5,7 @@ { "runtime": "automatic" } - ] + ], ], - "plugins": [] + "plugins": ["@babel/plugin-syntax-flow"] } diff --git a/apps/web-graphpolaris/src/app/app.tsx b/apps/web-graphpolaris/src/app/app.tsx index e94971f9d..4b52a8ebc 100644 --- a/apps/web-graphpolaris/src/app/app.tsx +++ b/apps/web-graphpolaris/src/app/app.tsx @@ -11,6 +11,7 @@ import { RawJSONVis } from '../components/visualisations/rawjsonvis/rawjsonvis'; import SemanticSubstrates from '../components/visualisations/semanticsubstrates/semanticsubstrates'; import LoginScreen from '../components/login/loginScreen'; import { OurThemeProvider } from '@graphpolaris/shared/data-access/theme'; +import Schema from '../components/schema/schema'; function useIsAuthorized() { const [userAuthorized, setUserAuthorized] = useState(false); @@ -51,7 +52,9 @@ export function App() { key="schema-panel" data-grid={{ x: 0, y: 0, w: 3, h: 30, maxW: 5, isDraggable: false }} > - <Panel content="Schema Panel" color="red"></Panel> + <Panel content="Schema Panel" color="red"> + <Schema /> + </Panel> </div> <div key="query-panel" diff --git a/apps/web-graphpolaris/src/components/schema/initial-elements.js b/apps/web-graphpolaris/src/components/schema/initial-elements.js new file mode 100644 index 000000000..09ea3488c --- /dev/null +++ b/apps/web-graphpolaris/src/components/schema/initial-elements.js @@ -0,0 +1,121 @@ +import React from 'react'; + +export default [ + { + id: '1', + type: 'input', + data: { label: 'Node 1' }, + position: { x: 250, y: 5 }, + }, + // you can also pass a React Node as a label + { id: '2', data: { label: <div>Node 2</div> }, position: { x: 100, y: 100 } }, + { id: 'e1-2', source: '1', target: '2', animated: true }, +]; + +// export default [ +// { +// id: '1', +// type: 'input', +// data: { +// label: ( +// <> +// Welcome to <strong>React Flow!</strong> +// </> +// ), +// }, +// position: { x: 250, y: 0 }, +// }, +// { +// id: '2', +// data: { +// label: ( +// <> +// This is a <strong>default node</strong> +// </> +// ), +// }, +// position: { x: 100, y: 100 }, +// }, +// { +// id: '3', +// data: { +// label: ( +// <> +// This one has a <strong>custom style</strong> +// </> +// ), +// }, +// position: { x: 400, y: 100 }, +// style: { +// background: '#D6D5E6', +// color: '#333', +// border: '1px solid #222138', +// width: 180, +// }, +// }, +// { +// id: '4', +// position: { x: 250, y: 200 }, +// data: { +// label: 'Another default node', +// }, +// }, +// { +// id: '5', +// data: { +// label: 'Node id: 5', +// }, +// position: { x: 250, y: 325 }, +// }, +// { +// id: '6', +// type: 'output', +// data: { +// label: ( +// <> +// An <strong>output node</strong> +// </> +// ), +// }, +// position: { x: 100, y: 480 }, +// }, +// { +// id: '7', +// type: 'output', +// data: { label: 'Another output node' }, +// position: { x: 400, y: 450 }, +// }, +// { id: 'e1-2', source: '1', target: '2', label: 'this is an edge label' }, +// { id: 'e1-3', source: '1', target: '3' }, +// { +// id: 'e3-4', +// source: '3', +// target: '4', +// animated: true, +// label: 'animated edge', +// }, +// { +// id: 'e4-5', +// source: '4', +// target: '5', +// // arrowHeadType: 'arrowclosed', +// label: 'edge with arrow head', +// }, +// { +// id: 'e5-6', +// source: '5', +// target: '6', +// type: 'smoothstep', +// label: 'smooth step edge', +// }, +// { +// id: 'e5-7', +// source: '5', +// target: '7', +// type: 'step', +// style: { stroke: '#f6ab6c' }, +// label: 'a step edge', +// animated: true, +// labelStyle: { fill: '#f6ab6c', fontWeight: 700 }, +// }, +// ]; diff --git a/apps/web-graphpolaris/src/components/schema/schema.stories.tsx b/apps/web-graphpolaris/src/components/schema/schema.stories.tsx index 7441b44b9..a38499b13 100644 --- a/apps/web-graphpolaris/src/components/schema/schema.stories.tsx +++ b/apps/web-graphpolaris/src/components/schema/schema.stories.tsx @@ -1,10 +1,16 @@ +import { parseSchemaFromBackend } from '@graphpolaris/schema/schema-usecases'; import { - // handleSchemaLayout, - parseSchemaFromBackend -} from '@graphpolaris/schema/schema-usecases'; -import { - store + SchemaFromBackend, + schemaSlice, + setSchema, + store, } from '@graphpolaris/shared/data-access/store'; +import { + movieSchema, + northWindSchema, + twitterSchema, +} from '@graphpolaris/shared/mock-data'; +import { configureStore } from '@reduxjs/toolkit'; import { ComponentMeta, ComponentStory } from '@storybook/react'; import React from 'react'; import { Provider } from 'react-redux'; @@ -20,7 +26,7 @@ export default { decorators: [ (story) => ( <div style={{ padding: '3rem' }}> - <Provider store={store}>{story()}</Provider> + <Provider store={Mockstore}>{story()}</Provider> </div> ), ], @@ -29,119 +35,15 @@ export default { const Template: ComponentStory<typeof Schema> = (args) => <Schema {...args} />; // // A super-simple mock of a redux store -// const Mockstore = configureStore({ -// reducer: { -// schema: schemaSlice, -// }, -// }); +const Mockstore = configureStore({ + reducer: { + schema: schemaSlice.reducer, + }, +}); export const TestWithSchema = Template.bind({}); TestWithSchema.play = async () => { - const dispatch = store.dispatch; - - const schema = parseSchemaFromBackend({ - nodes: [ - { - name: 'Thijs', - attributes: [], - }, - { - name: 'Airport', - attributes: [ - { name: 'city', type: 'string' }, - { name: 'vip', type: 'bool' }, - { name: 'state', type: 'string' }, - ], - }, - { - name: 'Airport2', - attributes: [ - { name: 'city', type: 'string' }, - { name: 'vip', type: 'bool' }, - { name: 'state', type: 'string' }, - ], - }, - { - name: 'Plane', - attributes: [ - { name: 'type', type: 'string' }, - { name: 'maxFuelCapacity', type: 'int' }, - ], - }, - { name: 'Staff', attributes: [] }, - ], - edges: [ - { - name: 'Airport2:Airport', - from: 'Airport2', - to: 'Airport', - collection: 'flights', - attributes: [ - { name: 'arrivalTime', type: 'int' }, - { name: 'departureTime', type: 'int' }, - ], - }, - { - name: 'Airport:Staff', - from: 'Airport', - to: 'Staff', - collection: 'flights', - attributes: [{ name: 'salary', type: 'int' }], - }, - { - name: 'Plane:Airport', - from: 'Plane', - to: 'Airport', - collection: 'flights', - attributes: [], - }, - { - name: 'Airport:Thijs', - from: 'Airport', - to: 'Thijs', - collection: 'flights', - attributes: [{ name: 'hallo', type: 'string' }], - }, - { - name: 'Thijs:Airport', - from: 'Thijs', - to: 'Airport', - collection: 'flights', - attributes: [{ name: 'hallo', type: 'string' }], - }, - { - name: 'Staff:Plane', - from: 'Staff', - to: 'Plane', - collection: 'flights', - attributes: [{ name: 'hallo', type: 'string' }], - }, - { - name: 'Staff:Airport2', - from: 'Staff', - to: 'Airport2', - collection: 'flights', - attributes: [{ name: 'hallo', type: 'string' }], - }, - { - name: 'Airport2:Plane', - from: 'Airport2', - to: 'Plane', - collection: 'flights', - attributes: [{ name: 'hallo', type: 'string' }], - }, - - { - name: 'Airport:Airport', - from: 'Airport', - to: 'Airport', - collection: 'flights', - attributes: [{ name: 'test', type: 'string' }], - }, - ], - }); - - //dispatch(setSchema(schema)); - // handleSchemaLayout(schema); + const parsed = parseSchemaFromBackend(twitterSchema as SchemaFromBackend); + Mockstore.dispatch(setSchema(parsed.export())); }; diff --git a/apps/web-graphpolaris/src/components/schema/schema.tsx b/apps/web-graphpolaris/src/components/schema/schema.tsx index 1023e2e3a..094621fb9 100644 --- a/apps/web-graphpolaris/src/components/schema/schema.tsx +++ b/apps/web-graphpolaris/src/components/schema/schema.tsx @@ -1,45 +1,63 @@ -import { useSchema } from '@graphpolaris/shared/data-access/store'; -//import { useEffect } from 'react'; -import ReactFlow from 'react-flow-renderer'; import { createReactFlowElements } from '@graphpolaris/schema/schema-usecases'; +import { + useSchema, + useSchemaLayout, +} from '@graphpolaris/shared/data-access/store'; +import { + AllLayoutAlgorithms, + LayoutFactory, +} from '@graphpolaris/shared/graph-layout'; +import { Attributes } from 'graphology-types'; import { useEffect, useState } from 'react'; +import ReactFlow, { + Node, + Edge, + Elements, + ReactFlowProvider, + FlowElement, +} from 'react-flow-renderer'; interface Props { - content: string; + // content: string; } -const initialElements = [ - { - id: '1', - type: 'input', - data: { label: 'Input Node' }, - position: { x: 250, y: 25 }, - }, - { - id: '2', - data: { label: 'Another Node' }, - position: { x: 100, y: 125 }, - }, -]; +const onLoad = (reactFlowInstance: any) => { + setTimeout(() => reactFlowInstance.fitView(), 0); +}; const Schema = (props: Props) => { + const [elements, setElements] = useState([] as FlowElement[]); + // In case the schema is updated const dbschema = useSchema(); - console.log(dbschema); + const schemaLayout = useSchemaLayout(); - const flowElements = createReactFlowElements(dbschema); - console.log(flowElements); + useEffect(() => { + const layoutFactory = new LayoutFactory(); - // const [dbschema, setSchema] = useState(useSchema()); + // console.log('schema Layout', schemaLayout); + const layout = layoutFactory.createLayout( + // schemaLayout as AllLayoutAlgorithms + 'Graphology_noverlap' + ); + layout?.layout(dbschema); - // const [flowElements, setFlowElements] = useState(initialElements); + // dbschema.forEachNode((node, attr) => { + // console.log('x', dbschema.getNodeAttribute(node, 'x')); + // console.log('y', dbschema.getNodeAttribute(node, 'y')); + // }); - // In case the schema is updated - // useEffect(() => { - // const flowElements = createReactFlowElements(dbschema); - // console.log('update schema useEffect'); - // }, [dbschema]); + const flowElements = createReactFlowElements(dbschema); + setElements(flowElements); + console.log( + 'update schema useEffect', + dbschema, + dbschema.order, + flowElements + ); + }, [dbschema, schemaLayout]); + + const graphStyles = { width: '100%', height: '500px' }; - // console.log(dbschema); return ( <div style={{ @@ -47,7 +65,16 @@ const Schema = (props: Props) => { height: '100%', }} > - <ReactFlow elements={createReactFlowElements(dbschema)} /> + {elements.length == 0 && <p>DEBUG: No Elements</p>} + <ReactFlowProvider> + <ReactFlow elements={elements} style={graphStyles} onLoad={onLoad} /> + {/* // onElementsRemove={onElementsRemove} + // onConnect={onConnect} + // + snapToGrid={true} + snapGrid={[15, 15]} + ></ReactFlow> */} + </ReactFlowProvider> </div> ); }; diff --git a/libs/schema/schema-usecases/src/lib/schema-usecases.spec.ts b/libs/schema/schema-usecases/src/lib/schema-usecases.spec.ts index a466f3101..87156cb18 100644 --- a/libs/schema/schema-usecases/src/lib/schema-usecases.spec.ts +++ b/libs/schema/schema-usecases/src/lib/schema-usecases.spec.ts @@ -1,14 +1,19 @@ -import { SchemaFromBackend } from '@graphpolaris/shared/data-access/store'; +import { + SchemaFromBackend, + setSchema, +} from '@graphpolaris/shared/data-access/store'; import { MultiGraph } from 'graphology'; -import { parse } from 'path/posix'; -import { parseSchemaFromBackend } from '..'; import { Attributes } from 'graphology-types'; import { movieSchema, northWindSchema, simpleSchema, twitterSchema, -} from 'libs/shared/mock-data/src'; +} from '@graphpolaris/shared/mock-data'; +import { parseSchemaFromBackend } from '..'; + +import { store } from '@graphpolaris/shared/data-access/store'; +import { LayoutFactory } from '@graphpolaris/shared/graph-layout'; describe('SchemaUsecases', () => { test.each([ @@ -67,4 +72,53 @@ describe('SchemaUsecases', () => { expect(data).toBeDefined(); expect(data.nodes).toBeDefined(); }); + + test.each([ + { data: simpleSchema }, + { data: movieSchema }, + { data: northWindSchema }, + { data: twitterSchema }, + ])('should load my test json $data', ({ data }) => { + const dispatch = store.dispatch; + + const parsed = parseSchemaFromBackend(data as SchemaFromBackend); + dispatch(setSchema(parsed.export())); + + const state = store.getState(); + const schema = state.schema.graphologySerialized; + expect(schema); + const graphReloaded = MultiGraph.from(schema); + + expect(graphReloaded).toStrictEqual(parsed); + }); + + test.each([ + { data: simpleSchema }, + { data: movieSchema }, + { data: northWindSchema }, + { data: twitterSchema }, + ])('should layout schema', ({ data }) => { + const parsed = parseSchemaFromBackend(data as SchemaFromBackend); + expect(parsed).toBeDefined(); + + const layout = store.getState().schema.schemaLayout; + expect(layout); + + // const carFactory = new CarFactory(); + const layouter = new LayoutFactory(); + + const layoutAlgorithm = layouter.createLayout('Graphology_noverlap'); + layoutAlgorithm?.layout(parsed); + + // const carFactory = new CarFactory(); + + // const carModels: AllCarModels[] = ['BMW_coupe', 'Audi_q4', 'Audi_etron', 'BMW_i4']; + + // const cars = carModels.map(model => carFactory.createCar(model)?.drive()); + + // const audi = carFactory.createCar('Audi_etron'); + // const bmw = carFactory.createCar('BMW_coupe'); + + // audi?.specialAudiFunction(); + }); }); diff --git a/libs/schema/schema-usecases/src/lib/schema-usecases.ts b/libs/schema/schema-usecases/src/lib/schema-usecases.ts index ea3fdc0fa..04dcbd855 100644 --- a/libs/schema/schema-usecases/src/lib/schema-usecases.ts +++ b/libs/schema/schema-usecases/src/lib/schema-usecases.ts @@ -1,8 +1,11 @@ import Graph, { MultiGraph } from 'graphology'; // import cytoscape from 'cytoscape'; // eslint-disable-line -import { setSchema, store } from '@graphpolaris/shared/data-access/store'; +import { + setSchema, + store, + SchemaFromBackend, +} from '@graphpolaris/shared/data-access/store'; import { Elements, Node, Edge } from 'react-flow-renderer'; -import { SchemaFromBackend } from '@graphpolaris/shared/data-access/store'; import { Attributes } from 'graphology-types'; type CytoNode = { diff --git a/libs/shared/data-access/store/src/lib/hooks.ts b/libs/shared/data-access/store/src/lib/hooks.ts index df0faef46..0d4265931 100644 --- a/libs/shared/data-access/store/src/lib/hooks.ts +++ b/libs/shared/data-access/store/src/lib/hooks.ts @@ -1,6 +1,6 @@ import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; import { selectGraphQueryResult } from './graphQueryResultSlice'; -import { selectSchema } from './schemaSlice'; +import { selectSchema, selectSchemaLayout } from './schemaSlice'; import type { RootState, AppDispatch } from './store'; // Use throughout your app instead of plain `useDispatch` and `useSelector` @@ -10,5 +10,13 @@ export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector; // Gives the graphQueryResult from the store export const useGraphQueryResult = () => useAppSelector(selectGraphQueryResult); -// Gives the schema form the store (as a graphology object) +/** + * + Gives the schema form the store (as a graphology object) + * */ export const useSchema = () => useAppSelector(selectSchema); + +/** + * Gives the schema form the store (as a enumeration of existing layout algorithms) + * */ +export const useSchemaLayout = () => useAppSelector(selectSchemaLayout); diff --git a/libs/shared/data-access/store/src/lib/schemaSlice.spec.ts b/libs/shared/data-access/store/src/lib/schemaSlice.spec.ts index 3a6b17a52..b0da5d51f 100644 --- a/libs/shared/data-access/store/src/lib/schemaSlice.spec.ts +++ b/libs/shared/data-access/store/src/lib/schemaSlice.spec.ts @@ -5,6 +5,13 @@ import reducer, { selectSchema, setSchema, initialState } from './schemaSlice'; // import { deleteBook, updateBook, addNewBook } from '../redux/bookSlice'; import { store } from './store'; +import { + movieSchema, + northWindSchema, + simpleSchema, + twitterSchema, +} from '@graphpolaris/shared/mock-data'; + describe('SchemaSlice Tests', () => { it('should make a graphology graph', () => { const graph = new MultiGraph({ allowSelfLoops: true }); @@ -31,7 +38,7 @@ describe('SchemaSlice Tests', () => { expect(initialState); }); - it('should return the initial state', () => { + it('state should return the initial state', () => { let state = store.getState(); const schema = state.schema; @@ -43,52 +50,19 @@ describe('SchemaSlice Tests', () => { // console.log(initialState); expect(graph); }); - - // it('should handle a todo being added to an empty list', () => { - // let state = store.getState().schema; - // let schema = useSchema(); - - // // const unchangedBook = state.bookList.find((book) => book.id === '1'); - // // expect(unchangedBook?.title).toBe('1984'); - // // expect(unchangedBook?.author).toBe('George Orwell'); - - // // store.dispatch(updateBook({ id: '1', title: '1985', author: 'George Bush' })); - // // state = store.getState().book; - // // let changeBook = state.bookList.find((book) => book.id === '1'); - // // expect(changeBook?.title).toBe('1985'); - // // expect(changeBook?.author).toBe('George Bush'); - - // // store.dispatch( - // // updateBook({ id: '1', title: '1984', author: 'George Orwell' }) - // // ); - // // state = store.getState().book; - // // const backToUnchangedBook = state.bookList.find((book) => book.id === '1'); - - // // expect(backToUnchangedBook).toEqual(unchangedBook); - // // ]); - // }); }); -// test('Deletes a book from list with id', () => { -// let state = store.getState().book; -// const initialBookCount = state.bookList.length; +test.each([ + { data: simpleSchema }, + { data: movieSchema }, + { data: northWindSchema }, + { data: twitterSchema }, +])('store and retrieve', ({ data }) => { -// store.dispatch(deleteBook({ id: '1' })); -// state = store.getState().book; + let state = store.getState(); -// expect(state.bookList.length).toBeLessThan(initialBookCount); // Checking if new length smaller than inital length, which is 3 -// }); + const schema = state.schema; + expect(schema); -// test('Adds a new book', () => { -// let state = store.getState().book; -// const initialBookCount = state.bookList.length; - -// store.dispatch( -// addNewBook({ id: '4', author: 'Tester', title: 'Testers manual' }) -// ); -// state = store.getState().book; -// const newlyAddedBook = state.bookList.find((book) => book.id === '4'); -// expect(newlyAddedBook?.author).toBe('Tester'); -// expect(newlyAddedBook?.title).toBe('Testers manual'); -// expect(state.bookList.length).toBeGreaterThan(initialBookCount); -// }); + expect(data).toBeDefined(); +}); diff --git a/libs/shared/data-access/store/src/lib/schemaSlice.ts b/libs/shared/data-access/store/src/lib/schemaSlice.ts index eb8fdb562..686cec2ee 100644 --- a/libs/shared/data-access/store/src/lib/schemaSlice.ts +++ b/libs/shared/data-access/store/src/lib/schemaSlice.ts @@ -1,6 +1,9 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import type { RootState } from './store'; -import Graph, { MultiGraph } from 'graphology'; +import { MultiGraph } from 'graphology'; +import { SerializedGraph } from 'graphology-types'; +import { AllLayoutAlgorithms } from '@graphpolaris/shared/graph-layout'; +import { Graphology } from 'libs/shared/graph-layout/src/factory/graphology-layouts'; /*************** schema format from the backend *************** */ // TODO: should probably not live here @@ -35,6 +38,7 @@ export type Edge = { // Define the initial state using that type export const initialState = { graphologySerialized: new MultiGraph().export(), + schemaLayout: 'Graphology_noverlap', }; export const schemaSlice = createSlice({ @@ -42,12 +46,10 @@ export const schemaSlice = createSlice({ // `createSlice` will infer the state type from the `initialState` argument initialState, reducers: { - setSchema: (state, action: PayloadAction<Graph>) => { - console.log('setSchema', action); - - state.graphologySerialized = action.payload.export(); + setSchema: (state, action: PayloadAction<SerializedGraph>) => { + // console.log('setSchema in schemaslice', action); + state.graphologySerialized = action.payload; }, - readInSchemaFromBackend: ( state, action: PayloadAction<SchemaFromBackend> @@ -89,13 +91,26 @@ export const schemaSlice = createSlice({ state.graphologySerialized = schema.export(); }, + setGraphLayout: (state, action: PayloadAction<AllLayoutAlgorithms>) => { + state.schemaLayout = action.payload; + }, }, }); export const { readInSchemaFromBackend, setSchema } = schemaSlice.actions; -// Select the schema and convert it to a graphology object +/** + * Select the schema and convert it to a graphology object + * */ export const selectSchema = (state: RootState) => - Graph.from(state.schema.graphologySerialized); + MultiGraph.from(state.schema.graphologySerialized).copy(); + +/** + * selects the SchemaLayout enum + * @param {GraphLayout} state + * @returns {GraphLayout} enum of type GraphLayout + */ +export const selectSchemaLayout = (state: RootState) => + state.schema.schemaLayout; export default schemaSlice.reducer; diff --git a/libs/shared/graph-layout/.babelrc b/libs/shared/graph-layout/.babelrc new file mode 100644 index 000000000..0cae4a9a8 --- /dev/null +++ b/libs/shared/graph-layout/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["@nrwl/web/babel"] +} diff --git a/libs/shared/graph-layout/.eslintrc.json b/libs/shared/graph-layout/.eslintrc.json new file mode 100644 index 000000000..3456be9b9 --- /dev/null +++ b/libs/shared/graph-layout/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/shared/graph-layout/README.md b/libs/shared/graph-layout/README.md new file mode 100644 index 000000000..b7f37ef4a --- /dev/null +++ b/libs/shared/graph-layout/README.md @@ -0,0 +1,7 @@ +# shared-graph-layout + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test shared-graph-layout` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/shared/graph-layout/jest.config.js b/libs/shared/graph-layout/jest.config.js new file mode 100644 index 000000000..489b3c0e0 --- /dev/null +++ b/libs/shared/graph-layout/jest.config.js @@ -0,0 +1,15 @@ +module.exports = { + displayName: 'shared-graph-layout', + preset: '../../../jest.preset.js', + globals: { + 'ts-jest': { + tsconfig: '<rootDir>/tsconfig.spec.json', + }, + }, + transform: { + '^.+\\.[tj]s$': 'ts-jest', + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../../coverage/libs/shared/graph-layout', + testEnvironment: 'jsdom', +}; diff --git a/libs/shared/graph-layout/project.json b/libs/shared/graph-layout/project.json new file mode 100644 index 000000000..b9148c494 --- /dev/null +++ b/libs/shared/graph-layout/project.json @@ -0,0 +1,29 @@ +{ + "root": "libs/shared/graph-layout", + "sourceRoot": "libs/shared/graph-layout/src", + "projectType": "library", + "targets": { + "lint": { + "executor": "@nrwl/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["libs/shared/graph-layout/**/*.ts"] + } + }, + "test": { + "executor": "@nrwl/jest:jest", + "outputs": ["coverage/libs/shared/graph-layout"], + "options": { + "jestConfig": "libs/shared/graph-layout/jest.config.js", + "passWithNoTests": true + } + }, + "version": { + "executor": "@jscutlery/semver:version", + "options": { + "commitMessageFormat": "chore(${projectName}): release version ${version}" + } + } + }, + "tags": [] +} diff --git a/libs/shared/graph-layout/src/factory/Layout.ts b/libs/shared/graph-layout/src/factory/Layout.ts new file mode 100644 index 000000000..06f4da35e --- /dev/null +++ b/libs/shared/graph-layout/src/factory/Layout.ts @@ -0,0 +1,21 @@ +import Graph from 'graphology'; +import { Providers, LayoutAlgorithm } from './layout-creator-usecase'; + +/** + * This is our Product + */ + +export abstract class Layout<provider extends Providers> { + constructor( + public provider: provider, + public algorithm: LayoutAlgorithm<provider> + ) { + console.log( + `Created the following Layout: ${provider} - ${this.algorithm}` + ); + } + + public layout(graph: Graph) { + console.log(`${this.provider} [${this.algorithm}] layouting now`); + } +} diff --git a/libs/shared/graph-layout/src/factory/cytoscape-layouts.ts b/libs/shared/graph-layout/src/factory/cytoscape-layouts.ts new file mode 100644 index 000000000..ce7800a50 --- /dev/null +++ b/libs/shared/graph-layout/src/factory/cytoscape-layouts.ts @@ -0,0 +1,54 @@ +import { Layout } from './Layout'; +import { ILayoutFactory, LayoutAlgorithm } from './layout-creator-usecase'; + +export type CytoscapeProvider = 'Cytoscape'; + +export type CytoscapeLayoutAlgorithms = + | `${CytoscapeProvider}_coupe` + | `${CytoscapeProvider}_i4`; + +/** + * This is a ConcreteCreator + */ +export class CytoscapeFactory + implements ILayoutFactory<CytoscapeLayoutAlgorithms> +{ + createLayout(LayoutAlgorithm: CytoscapeLayoutAlgorithms): Cytoscape | null { + switch (LayoutAlgorithm) { + case 'Cytoscape_coupe': + return new CytoscapeCoupe(); + case 'Cytoscape_i4': + return new CytoscapeI4(); + default: + return null; + } + } +} + +export abstract class Cytoscape extends Layout<CytoscapeProvider> { + constructor(public override algorithm: LayoutAlgorithm<CytoscapeProvider>) { + super('Cytoscape', algorithm); + } + + public specialCytoscapeFunction() { + console.log('Only Cytoscape Layouts can do this.'); + } +} + +/** + * This is a ConcreteProduct + */ +class CytoscapeI4 extends Cytoscape { + constructor() { + super('Cytoscape_i4'); + } +} + +/** + * This is a ConcreteProduct + */ +class CytoscapeCoupe extends Cytoscape { + constructor() { + super('Cytoscape_coupe'); + } +} diff --git a/libs/shared/graph-layout/src/factory/graphology-layouts.ts b/libs/shared/graph-layout/src/factory/graphology-layouts.ts new file mode 100644 index 000000000..ce9976091 --- /dev/null +++ b/libs/shared/graph-layout/src/factory/graphology-layouts.ts @@ -0,0 +1,169 @@ +import Graph from 'graphology'; +import { circular, random } from 'graphology-layout'; +import forceAtlas2, { + ForceAtlas2Settings, +} from 'graphology-layout-forceatlas2'; +import noverlap from 'graphology-layout-noverlap'; +import { RandomLayoutOptions } from 'graphology-layout/random'; +import { NoverlapSettings } from 'graphology-library/layout-noverlap'; +import { Attributes } from 'graphology-types'; +import { Layout } from './Layout'; +import { ILayoutFactory, LayoutAlgorithm } from './layout-creator-usecase'; + +export type GraphologyProvider = 'Graphology'; + +export type GraphologyLayoutAlgorithms = + | `${GraphologyProvider}_circular` + | `${GraphologyProvider}_random` + | `${GraphologyProvider}_noverlap` + | `${GraphologyProvider}_forceAtlas2`; + +/** + * This is a ConcreteCreator + */ +export class GraphologyFactory + implements ILayoutFactory<GraphologyLayoutAlgorithms> +{ + createLayout(layoutAlgorithm: GraphologyLayoutAlgorithms): Graphology | null { + switch (layoutAlgorithm) { + case 'Graphology_random': + return new GraphologyRandom(); + case 'Graphology_circular': + return new GraphologyCircular(); + case 'Graphology_noverlap': + return new GraphologyNoverlap(); + case 'Graphology_forceAtlas2': + return new GraphologyForceAtlas2(); + default: + return null; + } + } +} + +export abstract class Graphology extends Layout<GraphologyProvider> { + height: number = 100; + width: number = 100; + constructor(public override algorithm: LayoutAlgorithm<GraphologyProvider>) { + super('Graphology', algorithm); + this.setDimensions(100, 200); + } + + public specialGraphologyFunction() { + // graph.forEachNode((node, attr) => { + // graph.setNodeAttribute(node, 'x', 0); + // graph.setNodeAttribute(node, 'y', 0); + // }); + } + + public setDimensions(width = 100, height = 100) { + this.width = width; + this.height = height; + } +} + +/** + * This is a ConcreteProduct + */ +export class GraphologyCircular extends Graphology { + constructor() { + super('Graphology_circular'); + } + + public override layout( + graph: Graph<Attributes, Attributes, Attributes> + ): void { + // To directly assign the positions to the nodes: + circular.assign(graph, { + scale: 100, + }); + } +} + +const DEFAULT_RANDOM_SETTINGS: RandomLayoutOptions = { + scale: 250, +}; +/** + * This is a ConcreteProduct + */ +export class GraphologyRandom extends Graphology { + constructor() { + super('Graphology_random'); + } + + public override layout( + graph: Graph<Attributes, Attributes, Attributes> + ): void { + // const positions = random(graph); + + // To directly assign the positions to the nodes: + random.assign(graph, DEFAULT_RANDOM_SETTINGS); + } +} + +const DEFAULT_NOVERLAP_SETTINGS: NoverlapSettings = { + margin: 40, + ratio: 40, + // gridSize: 50, + + // gridSize ?number 20: number of grid cells horizontally and vertically subdivising the graph’s space. This is used as an optimization scheme. Set it to 1 and you will have O(n²) time complexity, which can sometimes perform better with very few nodes. + // margin ?number 5: margin to keep between nodes. + // expansion ?number 1.1: percentage of current space that nodes could attempt to move outside of. + // ratio ?number 1.0: ratio scaling node sizes. + // speed ?number 3: dampening factor that will slow down node movements to ease the overall process. +}; + +/** + * This is a ConcreteProduct + */ +export class GraphologyNoverlap extends Graphology { + constructor() { + super('Graphology_noverlap'); + } + + public override layout( + graph: Graph<Attributes, Attributes, Attributes> + ): void { + // // // To directly assign the positions to the nodes: + noverlap.assign(graph, { + maxIterations: 5000, + settings: DEFAULT_NOVERLAP_SETTINGS, + }); + } +} + +const DEFAULT_FORCEATLAS2_SETTINGS: ForceAtlas2Settings = { + gravity: 10, + adjustSizes: true, + linLogMode: true, + + // adjustSizes ?boolean false: should the node’s sizes be taken into account? + // barnesHutOptimize ?boolean false: whether to use the Barnes-Hut approximation to compute repulsion in O(n*log(n)) rather than default O(n^2), n being the number of nodes. + // barnesHutTheta ?number 0.5: Barnes-Hut approximation theta parameter. + // edgeWeightInfluence ?number 1: influence of the edge’s weights on the layout. To consider edge weight, don’t forget to pass weighted as true when applying the synchronous layout or when instantiating the worker. + // gravity ?number 1: strength of the layout’s gravity. + // linLogMode ?boolean false: whether to use Noack’s LinLog model. + // outboundAttractionDistribution ?boolean false + // scalingRatio ?number 1 + // slowDown ?number 1 + // strongGravityMode ?boolean false +}; + +/** + * This is a ConcreteProduct + */ +export class GraphologyForceAtlas2 extends Graphology { + constructor() { + super('Graphology_forceAtlas2'); + } + + public override layout( + graph: Graph<Attributes, Attributes, Attributes> + ): void { + // To directly assign the positions to the nodes: + + forceAtlas2.assign(graph, { + iterations: 500000, + settings: DEFAULT_FORCEATLAS2_SETTINGS, + }); + } +} diff --git a/libs/shared/graph-layout/src/factory/layout-creator-usecase.spec.ts b/libs/shared/graph-layout/src/factory/layout-creator-usecase.spec.ts new file mode 100644 index 000000000..e9a0771d5 --- /dev/null +++ b/libs/shared/graph-layout/src/factory/layout-creator-usecase.spec.ts @@ -0,0 +1,142 @@ +import { + AllLayoutAlgorithms, + LayoutFactory, +} from '@graphpolaris/shared/graph-layout'; + +import { + movieSchema, + northWindSchema, + simpleSchema, + twitterSchema, +} from '@graphpolaris/shared/mock-data'; +import Graph, { MultiGraph } from 'graphology'; + +import ladder from 'graphology-generators/classic/ladder'; + +/** + * @jest-environment jsdom + */ +describe('LayoutFactory', () => { + /** + * @jest-environment jsdom + */ + it('should work with noverlap from graphology ', () => { + const graph = new MultiGraph(); + + // Adding some nodes + graph.addNode('John', { x: 0, y: 0 }); + graph.addNode('Martha', { x: 0, y: 0 }); + + // Adding an edge + graph.addEdge('John', 'Martha'); + + const layoutFactory = new LayoutFactory(); + + const layout = layoutFactory.createLayout('Graphology_noverlap'); + + layout?.layout(graph); + + graph.forEachNode((node, attr) => { + console.log(node, attr); + }); + }); + + test('should work with noverlap from graphology on generated graph', () => { + // Creating a ladder graph + const graph = ladder(Graph, 10); + + graph.forEachNode((node, attr) => { + graph.setNodeAttribute(node, 'x', 0); + graph.setNodeAttribute(node, 'y', 0); + }); + + const layoutFactory = new LayoutFactory(); + + const layout = layoutFactory.createLayout('Graphology_noverlap'); + layout?.layout(graph); + + graph.forEachNode((node, attr) => { + console.log(node, attr); + }); + }); + + test('should work with random from graphology on generated graph', () => { + // Creating a ladder graph + const graph = ladder(Graph, 10); + + graph.forEachNode((node, attr) => { + graph.setNodeAttribute(node, 'x', 0); + graph.setNodeAttribute(node, 'y', 0); + }); + + const layoutFactory = new LayoutFactory(); + + const layout = layoutFactory.createLayout('Graphology_random'); + layout?.setDimensions(100, 100); + layout?.layout(graph); + + graph.forEachNode((node, attr) => { + console.log(node, attr); + }); + }); + + test('should work with circular from graphology on generated graph', () => { + // Creating a ladder graph + const graph = ladder(Graph, 100); + + graph.forEachNode((node, attr) => { + graph.setNodeAttribute(node, 'x', 0); + graph.setNodeAttribute(node, 'y', 0); + }); + + const layoutFactory = new LayoutFactory(); + + const layout = layoutFactory.createLayout('Graphology_circular'); + layout?.setDimensions(100, 100); + layout?.layout(graph); + + graph.forEachNode((node, attr) => { + console.log(node, attr); + }); + }); + + test('should work with circular from graphology on generated graph', () => { + // Creating a ladder graph + const graph = ladder(Graph, 100); + + graph.forEachNode((node, attr) => { + graph.setNodeAttribute(node, 'x', 0); + graph.setNodeAttribute(node, 'y', 0); + }); + + const layoutFactory = new LayoutFactory(); + const layout = layoutFactory.createLayout('Graphology_forceAtlas2'); + layout?.setDimensions(100, 100); + layout?.layout(graph); + + graph.forEachNode((node, attr) => { + console.log(node, attr); + }); + }); + + test('should add x,y for graphology layouts if not existing', () => { + // console.log(Object.keys(AllLayoutAlgorithms)) + + const graph = ladder(Graph, 100); + + graph.forEachNode((node, attr) => { + expect(graph.getNodeAttribute(node, 'x')).toBeUndefined(); + expect(graph.getNodeAttribute(node, 'y')).toBeUndefined(); + }); + + const layoutFactory = new LayoutFactory(); + const layout = layoutFactory.createLayout('Graphology_forceAtlas2'); + layout?.setDimensions(100, 100); + layout?.layout(graph); + + graph.forEachNode((node, attr) => { + expect(graph.getNodeAttribute(node, 'x')).toBeDefined(); + expect(graph.getNodeAttribute(node, 'y')).toBeDefined(); + }); + }); +}); diff --git a/libs/shared/graph-layout/src/factory/layout-creator-usecase.ts b/libs/shared/graph-layout/src/factory/layout-creator-usecase.ts new file mode 100644 index 000000000..53a6411db --- /dev/null +++ b/libs/shared/graph-layout/src/factory/layout-creator-usecase.ts @@ -0,0 +1,77 @@ +import { + Cytoscape, + CytoscapeFactory, + CytoscapeLayoutAlgorithms, + CytoscapeProvider, +} from './cytoscape-layouts'; +import { + Graphology, + GraphologyFactory, + GraphologyLayoutAlgorithms, + GraphologyProvider, +} from './graphology-layouts'; +import { Layout } from './Layout'; + +export type Providers = GraphologyProvider | CytoscapeProvider; +export type LayoutAlgorithm<Provider extends Providers> = + `${Provider}_${string}`; + +export type AllLayoutAlgorithms = + | GraphologyLayoutAlgorithms + | CytoscapeLayoutAlgorithms; + +export type AlgorithmToLayoutProvider<Algorithm extends AllLayoutAlgorithms> = + Algorithm extends GraphologyLayoutAlgorithms + ? Graphology + : Algorithm extends CytoscapeLayoutAlgorithms + ? Cytoscape + : Cytoscape | Graphology; + +export interface ILayoutFactory<Algorithm extends AllLayoutAlgorithms> { + createLayout: ( + Algorithm: Algorithm + ) => AlgorithmToLayoutProvider<Algorithm> | null; +} + +/** + * This is our Creator + */ +export class LayoutFactory implements ILayoutFactory<AllLayoutAlgorithms> { + private graphologyFactory = new GraphologyFactory(); + private cytoscapeFactory = new CytoscapeFactory(); + + private isSpecificAlgorithm<Algorithm extends AllLayoutAlgorithms>( + LayoutAlgorithm: AllLayoutAlgorithms, + startsWith: string + ): LayoutAlgorithm is Algorithm { + return LayoutAlgorithm.startsWith(startsWith); + } + + createLayout<Algorithm extends AllLayoutAlgorithms>( + layoutAlgorithm: Algorithm + ): AlgorithmToLayoutProvider<Algorithm> | null { + if ( + this.isSpecificAlgorithm<GraphologyLayoutAlgorithms>( + layoutAlgorithm, + 'Graphology' + ) + ) { + return this.graphologyFactory.createLayout( + layoutAlgorithm + ) as AlgorithmToLayoutProvider<Algorithm>; + } + + if ( + this.isSpecificAlgorithm<CytoscapeLayoutAlgorithms>( + layoutAlgorithm, + 'Cytoscape' + ) + ) { + return this.cytoscapeFactory.createLayout( + layoutAlgorithm + ) as AlgorithmToLayoutProvider<Algorithm>; + } + + return null; + } +} diff --git a/libs/shared/graph-layout/src/factory/test.ts b/libs/shared/graph-layout/src/factory/test.ts new file mode 100644 index 000000000..e69de29bb diff --git a/libs/shared/graph-layout/src/index.ts b/libs/shared/graph-layout/src/index.ts new file mode 100644 index 000000000..7bdb94a4e --- /dev/null +++ b/libs/shared/graph-layout/src/index.ts @@ -0,0 +1,6 @@ +// export * from './factory/layout-creator-usecase'; + +export { + AllLayoutAlgorithms, + LayoutFactory, +} from './factory/layout-creator-usecase'; diff --git a/libs/shared/graph-layout/tsconfig.json b/libs/shared/graph-layout/tsconfig.json new file mode 100644 index 000000000..648c1247a --- /dev/null +++ b/libs/shared/graph-layout/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "compilerOptions": { + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "lib": ["webworker", "scripthost"], + "esModuleInterop": true, + "composite": true + } +} diff --git a/libs/shared/graph-layout/tsconfig.lib.json b/libs/shared/graph-layout/tsconfig.lib.json new file mode 100644 index 000000000..6eb3eb9ea --- /dev/null +++ b/libs/shared/graph-layout/tsconfig.lib.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "types": [], + "composite": true + }, + "include": ["**/*.ts"], + "exclude": ["**/*.spec.ts"] +} diff --git a/libs/shared/graph-layout/tsconfig.spec.json b/libs/shared/graph-layout/tsconfig.spec.json new file mode 100644 index 000000000..d8716fecf --- /dev/null +++ b/libs/shared/graph-layout/tsconfig.spec.json @@ -0,0 +1,19 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "**/*.test.ts", + "**/*.spec.ts", + "**/*.test.tsx", + "**/*.spec.tsx", + "**/*.test.js", + "**/*.spec.js", + "**/*.test.jsx", + "**/*.spec.jsx", + "**/*.d.ts" + ] +} diff --git a/package.json b/package.json index a400f2140..c2b43c245 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "core-js": "^3.6.5", "cytoscape": "^3.21.0", "graphology": "^0.24.0", + "graphology-library": "^0.7.1", "graphology-types": "^0.24.0", "react": "17.0.2", "react-cookie": "^4.1.1", @@ -35,6 +36,8 @@ }, "devDependencies": { "@babel/core": "7.12.13", + "@babel/plugin-syntax-flow": "^7.16.7", + "@babel/preset-flow": "^7.16.7", "@babel/preset-typescript": "7.12.13", "@commitlint/cli": "^16.0.1", "@commitlint/config-angular": "^16.0.0", diff --git a/tsconfig.base.json b/tsconfig.base.json index 60c712ba8..f20c2ad21 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -28,6 +28,10 @@ "libs/shared/data-access/theme/src/index.ts" ], "@graphpolaris/shared/mock-data": ["libs/shared/mock-data/src/index.ts"], + "@graphpolaris/shared/graph-layout": [ + "libs/shared/graph-layout/src/index.ts" + ], + "@mui/styled-engine": ["./node_modules/@mui/styled-engine-sc"] } }, diff --git a/yarn.lock b/yarn.lock index 3d6d637e5..898701c93 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1133,7 +1133,7 @@ core-js-compat "^3.20.2" semver "^6.3.0" -"@babel/preset-flow@^7.12.1": +"@babel/preset-flow@^7.12.1", "@babel/preset-flow@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/preset-flow/-/preset-flow-7.16.7.tgz#7fd831323ab25eeba6e4b77a589f680e30581cbd" integrity sha512-6ceP7IyZdUYQ3wUVqyRSQXztd1YmFHWI4Xv11MIqAlE4WqxBSd/FZ61V9k+TS5Gd4mkHOtQtPp9ymRpxH4y1Ug== @@ -4929,6 +4929,11 @@ "@webassemblyjs/wast-parser" "1.9.0" "@xtuc/long" "4.2.2" +"@xmldom/xmldom@^0.7.5": + version "0.7.5" + resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.7.5.tgz#09fa51e356d07d0be200642b0e4f91d8e6dd408d" + integrity sha512-V3BIhmY36fXZ1OtVcI9W+FxQqxVLsPKcNjWigIaa81dLC9IolJl5Mt4Cvhmr0flUnjSpTdrbMTSbXqYqV5dT6A== + "@xtuc/ieee754@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" @@ -4949,6 +4954,11 @@ tslib "^2.3.1" upath2 "^3.1.12" +"@yomguithereal/helpers@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@yomguithereal/helpers/-/helpers-1.1.1.tgz#185dfb0f88ca2beec53d0adf6eed15c33b1c549d" + integrity sha512-UYvAq/XCA7xoh1juWDYsq3W0WywOB+pz8cgVnE1b45ZfdMhBvHDrgmSFG3jXeZSr2tMTYLGHFHON+ekG05Jebg== + JSONStream@^1.0.4: version "1.3.5" resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" @@ -9567,11 +9577,185 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6 resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96" integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ== +graphology-assertions@~2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/graphology-assertions/-/graphology-assertions-2.2.1.tgz#2fdc64a26434a2aac2de3d6d4ef889a0005ff379" + integrity sha512-X6yvm8eYDepIyywDM/K0ud/NDD6I5aZj5+D459z4wZFEtb8B33A4NNPsMpjMNwjNdEo1q4VL1Gqp/usoItEP4g== + dependencies: + fast-deep-equal "^3.1.3" + graphology-utils "^2.1.2" + +graphology-communities-louvain@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/graphology-communities-louvain/-/graphology-communities-louvain-2.0.0.tgz#ef93a89e0122078ce77676f5351731ee7eb58df9" + integrity sha512-zHqfPBh43XHs5Pp/u2XwdXrPmMLGBEdlgXqciowpa/dsD/VOC7pnD9HquJ3dGuLa2V2cdUAesOehFDb2M7soeA== + dependencies: + graphology-indices "^0.16.4" + graphology-utils "^2.4.4" + mnemonist "^0.39.0" + pandemonium "^2.3.0" + +graphology-components@~1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/graphology-components/-/graphology-components-1.5.2.tgz#a2bdf0f3e09eee1e3d5913a95442cadcd74b912f" + integrity sha512-EW26mjHWX9sggUhZcW1OHsOnEV7lj0nx50mcEHFRNucC3MBoe4yDYtBY8HQqUcGH4FdEq0ukNzzweJGLiy58Tg== + dependencies: + graphology-indices "^0.16.2" + graphology-utils "^2.1.2" + +graphology-generators@~0.11.2: + version "0.11.2" + resolved "https://registry.yarnpkg.com/graphology-generators/-/graphology-generators-0.11.2.tgz#eff2c97e4f5bf401e86ab045470dded95f2ebe24" + integrity sha512-hx+F0OZRkVdoQ0B1tWrpxoakmHZNex0c6RAoR0PrqJ+6fz/gz6CQ88Qlw78C6yD9nlZVRgepIoDYhRTFV+bEHg== + dependencies: + graphology-metrics "^2.0.0" + graphology-utils "^2.3.0" + +graphology-gexf@~0.10.1: + version "0.10.1" + resolved "https://registry.yarnpkg.com/graphology-gexf/-/graphology-gexf-0.10.1.tgz#55165379945bc5e1435ab8c12cb51052792c8d58" + integrity sha512-vNBn5eVOWRSiedwAFVBehuA0KxzOzorBMvRW2md01UZcaVVh0BRzB6uFEB6+QHmdRqtpewhCQ6RQUifQ8r7btg== + dependencies: + "@xmldom/xmldom" "^0.7.5" + graphology-operators "^1.5.0" + graphology-utils "^2.4.1" + xml-writer "^1.7.0" + +graphology-graphml@^0.5.0: + version "0.5.1" + resolved "https://registry.yarnpkg.com/graphology-graphml/-/graphology-graphml-0.5.1.tgz#d6573694b16e8daecb3fd33196daf21a92fafa51" + integrity sha512-h2bpxlMtSC5lxQuv53r2i4sGqapAmeGUsfjbPmIH1v+y2BiEq4kLbqZuE1JKTD5oXQJeSsHksXKhN8pMGXOnxQ== + dependencies: + "@xmldom/xmldom" "^0.7.5" + graphology-operators "^1.5.0" + graphology-utils "^2.4.1" + xml-writer "^1.7.0" + +graphology-indices@^0.16.2, graphology-indices@^0.16.3, graphology-indices@^0.16.4: + version "0.16.6" + resolved "https://registry.yarnpkg.com/graphology-indices/-/graphology-indices-0.16.6.tgz#0de112ef0367e44041490933e34ad2075cb24e80" + integrity sha512-tozTirLb7pd37wULJ5qeIZfZqKuVln/V+bWmUWJ7MmoTU8YkW5dehOkRz2by/O+5MdJ52imqL8LH4+GCd0yEVw== + dependencies: + graphology-utils "^2.4.2" + mnemonist "^0.39.0" + +graphology-layout-force@~0.2.3: + version "0.2.4" + resolved "https://registry.yarnpkg.com/graphology-layout-force/-/graphology-layout-force-0.2.4.tgz#a9b5f2aa5c7b56985503d302dbce4c73c76b9eb3" + integrity sha512-NYZz0YAnDkn5pkm30cvB0IScFoWGtbzJMrqaiH070dYlYJiag12Oc89dbVfaMaVR/w8DMIKxn/ix9Bqj+Umm9Q== + dependencies: + graphology-utils "^2.4.2" + +graphology-layout-forceatlas2@~0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/graphology-layout-forceatlas2/-/graphology-layout-forceatlas2-0.8.1.tgz#bb6f34f5181616bb25ea7a8adec73c79aef2aa8d" + integrity sha512-lAm9T0uBxhECZTVyYDMMnPi3l7h5kG2+7yfxqoT9wpgF/omComGc6vR9wmQqClQjSXiM3OU4frO4j2Il5E72Xg== + dependencies: + graphology-utils "^2.1.0" + +graphology-layout-noverlap@^0.4.1: + version "0.4.2" + resolved "https://registry.yarnpkg.com/graphology-layout-noverlap/-/graphology-layout-noverlap-0.4.2.tgz#2ffa054ceeebaa31fcffe695d271fc55707cd29c" + integrity sha512-13WwZSx96zim6l1dfZONcqLh3oqyRcjIBsqz2c2iJ3ohgs3605IDWjldH41Gnhh462xGB1j6VGmuGhZ2FKISXA== + dependencies: + graphology-utils "^2.3.0" + +graphology-layout@~0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/graphology-layout/-/graphology-layout-0.5.0.tgz#a0a54861cebae5f486c778dbdafc6294859f23b5" + integrity sha512-aIeXYPLeGMLvXIkO41TlhBv0ROFWUx1bqR2VQoJ7Mp2IW+TF+rxqMeRUrmyLHoe3HtKo8jhloB2KHp7g6fcDSg== + dependencies: + graphology-utils "^2.3.0" + pandemonium "^1.5.0" + +graphology-library@^0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/graphology-library/-/graphology-library-0.7.1.tgz#9fdec0f6d00f5207895dc9b7e299e454bb3c52f8" + integrity sha512-pKqaMiuKNAaVwYQAL9I3EVADYOh1tcWe1NfxzmoSGWLYdHvalNGAI+c63Wd4DG021He2YUYR6yB5gtccqVUS3Q== + dependencies: + graphology-assertions "~2.2.1" + graphology-communities-louvain "~2.0.0" + graphology-components "~1.5.2" + graphology-generators "~0.11.2" + graphology-gexf "~0.10.1" + graphology-graphml "^0.5.0" + graphology-layout "~0.5.0" + graphology-layout-force "~0.2.3" + graphology-layout-forceatlas2 "~0.8.1" + graphology-layout-noverlap "^0.4.1" + graphology-metrics "~2.0.0" + graphology-operators "~1.5.0" + graphology-shortest-path "~2.0.0" + graphology-simple-path "^0.1.2" + graphology-traversal "^0.3.0" + graphology-utils "~2.5.0" + +graphology-metrics@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/graphology-metrics/-/graphology-metrics-2.1.0.tgz#7d00bae92d8970583afd020e6d40d8a16c378002" + integrity sha512-E+y4kgVGxhYl/+bPHEftJeWLS8LgVno4/Wvg+C7IoDIjY6OlIZghgMKDR8LKsxU6GC43mlx08FTZs229cvEkwQ== + dependencies: + graphology-shortest-path "^2.0.0" + graphology-utils "^2.4.4" + mnemonist "^0.39.0" + +graphology-metrics@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/graphology-metrics/-/graphology-metrics-2.0.0.tgz#af2bfcadd1d5842d14c9a6092175e22fbab7024f" + integrity sha512-vh2XaAaiB9Of92ac2imaztFjPHmMIotuIN/rNt/X+DqJhxpuyOp+Ir3fQPVJScvk11zMWOimTsMdLYt1gbXWeQ== + dependencies: + graphology-shortest-path "^2.0.0" + graphology-utils "^2.4.4" + mnemonist "^0.39.0" + +graphology-operators@^1.5.0, graphology-operators@~1.5.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/graphology-operators/-/graphology-operators-1.5.1.tgz#67f4447df21baa59b571ab7a4c688a2302f9ce20" + integrity sha512-VojhTwtKlGtbopHPzOmAsAgM8MJY1HScgAs3G8FobtI+xsSlnFSKQeuWIibXEg6/wwPHGYT2oxMJbgHcwAEr3Q== + dependencies: + graphology-utils "^2.0.0" + +graphology-shortest-path@^2.0.0, graphology-shortest-path@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/graphology-shortest-path/-/graphology-shortest-path-2.0.0.tgz#27a01b3a9980872bd44a197fc77114623dd2b302" + integrity sha512-6dJWgbr7w4YQKb7Y0w7vhZn2qAkqP+J0IhE9F3vz/HZcx7VSOqnNfTGtYr44BQ5ohdXj0l9iKjlWCb+3vqEINQ== + dependencies: + "@yomguithereal/helpers" "^1.1.1" + graphology-indices "^0.16.3" + graphology-utils "^2.4.3" + mnemonist "^0.39.0" + +graphology-simple-path@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/graphology-simple-path/-/graphology-simple-path-0.1.2.tgz#b8d84852c94a069a8e906faa5274d33df0a53419" + integrity sha512-jOut2ihx5XMN97eUtmy4ZMp22btx3oa8GnvzQXHiBZOMyaC/gCpupnKVh0IvtzKd0RmmC5lT0zPBAqvU2O7Ejg== + dependencies: + graphology-utils "^1.8.0" + mnemonist "^0.39.0" + +graphology-traversal@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/graphology-traversal/-/graphology-traversal-0.3.0.tgz#1045af9e35a86932bc964caf110b45338769cfcb" + integrity sha512-/dexAoGbaRqmBMtv9IBXUuLZg7b5YBpWYXZYirwRaFX0DsQmsqMAJ+Jx0RQndQqLCOJI3LZyheemv3+tLlrjjQ== + dependencies: + graphology-indices "^0.16.4" + graphology-utils "^2.0.0" + graphology-types@^0.24.0: version "0.24.0" resolved "https://registry.yarnpkg.com/graphology-types/-/graphology-types-0.24.0.tgz#81aaef55226edb692dd63a9ce5eaecc80694cd93" integrity sha512-3qSanRtucm6rwBjpwuAc18GQcl68NqIRE4OA3wFUzdB2HRVXYoCAUsUJVS898bW+byEgd+BTcwR26CbltNvSWQ== +graphology-utils@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/graphology-utils/-/graphology-utils-1.8.0.tgz#41315c468656e2a3e028a76468bbc2fbe42b0145" + integrity sha512-Pa7SW30OMm8fVtyH49b3GJ/uxlMHGfXly50wIhlcc7ZoX9ahZa7sPBz+obo4WZClrRV6wh3tIu0GJoI42eao1A== + +graphology-utils@^2.0.0, graphology-utils@^2.1.0, graphology-utils@^2.1.2, graphology-utils@^2.3.0, graphology-utils@^2.4.1, graphology-utils@^2.4.2, graphology-utils@^2.4.3, graphology-utils@^2.4.4, graphology-utils@~2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/graphology-utils/-/graphology-utils-2.5.0.tgz#ccb0ec8231e4de403ed7a385644c911e40bc4833" + integrity sha512-TmuBAoM1rZxWo3Wd7qC2Rhnu3KZwq8pWNgjWCFKubn3pt3a1Vh/k3CJaFw4G7k6Mvb6aSdWVYJnlGNThMl+bAQ== + graphology@^0.24.0: version "0.24.0" resolved "https://registry.yarnpkg.com/graphology/-/graphology-0.24.0.tgz#c3c78b197f8ff6d8d3422a2d705c16e637b295f6" @@ -12284,6 +12468,13 @@ mkdirp@^1.0.3, mkdirp@^1.0.4, mkdirp@~1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== +mnemonist@^0.39.0: + version "0.39.0" + resolved "https://registry.yarnpkg.com/mnemonist/-/mnemonist-0.39.0.tgz#4c83dd22e8d9d05dfb721ff66a905fec4c460041" + integrity sha512-7v08Ldk1lnlywnIShqfKYN7EW4WKLUnkoWApdmR47N1xA2xmEtWERfEvyRCepbuFCETG5OnfaGQpp/p4Bus6ZQ== + dependencies: + obliterator "^2.0.1" + modify-values@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" @@ -12701,7 +12892,7 @@ objectorarray@^1.0.5: resolved "https://registry.yarnpkg.com/objectorarray/-/objectorarray-1.0.5.tgz#2c05248bbefabd8f43ad13b41085951aac5e68a5" integrity sha512-eJJDYkhJFFbBBAxeh8xW+weHlkI28n2ZdQV/J/DNfWfSKlGEf2xcfAbZTv3riEXHAhL9SVOTs2pRmXiSTf78xg== -obliterator@^2.0.2: +obliterator@^2.0.1, obliterator@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/obliterator/-/obliterator-2.0.2.tgz#25f50dc92e1181371b9d8209d11890f1a3c2fc21" integrity sha512-g0TrA7SbUggROhDPK8cEu/qpItwH2LSKcNl4tlfBNT54XY+nOsqrs0Q68h1V9b3HOSpIWv15jb1lax2hAggdIg== @@ -12955,6 +13146,18 @@ pako@~1.0.5: resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== +pandemonium@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/pandemonium/-/pandemonium-1.5.0.tgz#93f35af555de1420022b341e730215c51c725be3" + integrity sha512-9PU9fy93rJhZHLMjX+4M1RwZPEYl6g7DdWKGmGNhkgBZR5+tOBVExNZc00kzdEGMxbaAvWdQy9MqGAScGwYlcA== + +pandemonium@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pandemonium/-/pandemonium-2.3.0.tgz#d9c70686bb33c3ee4f435162601b0755a439bdcb" + integrity sha512-/+5rFCn3npqNwAvhKOSRKwAnEWQeXH4xFuur7vWKlj5Z7AM2JNJt1tC2rptfm1M7t7OlXAyeBuRjspqe+gQGHA== + dependencies: + mnemonist "^0.39.0" + parallel-transform@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.2.0.tgz#9049ca37d6cb2182c3b1d2c720be94d14a5814fc" @@ -17111,6 +17314,11 @@ xml-name-validator@^3.0.0: resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== +xml-writer@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/xml-writer/-/xml-writer-1.7.0.tgz#b76f1d591c16a2634ebdb703c7bdbd0fd6819065" + integrity sha1-t28dWRwWomNOvbcDx729D9aBkGU= + xmlchars@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" -- GitLab