Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • graphpolaris/frontend-v2
  • rijkheere/frontend-v-2-reordering-paoh
2 results
Show changes
Commits on Source (7)
Showing
with 519 additions and 99 deletions
...@@ -8,4 +8,16 @@ VITE_BACKEND_QUERY=/query ...@@ -8,4 +8,16 @@ VITE_BACKEND_QUERY=/query
VITE_BACKEND_SCHEMA=/schema VITE_BACKEND_SCHEMA=/schema
SENTRY_ENABLED=false SENTRY_ENABLED=false
SENTRY_URL= SENTRY_URL=
\ No newline at end of file
WIP_TABLEVIS=false
WIP_NODELINKVIS=false
WIP_RAWJSONVIS=false
WIP_PAOHVIS=true
WIP_MATRIXVIS=true
WIP_SEMANTICSUBSTRATESVIS=true
WIP_MAPVIS=true
WIP_INSIGHT_SHARING=true
WIP_VIEWER_PERMISSIONS=true
WIP_SHARABLE_EXPLORATION=true
\ No newline at end of file
...@@ -7,7 +7,7 @@ import { ...@@ -7,7 +7,7 @@ import {
useQuerybuilderSettings, useQuerybuilderSettings,
useSessionCache, useSessionCache,
} from '@graphpolaris/shared/lib/data-access'; } from '@graphpolaris/shared/lib/data-access';
import { setCurrentTheme } from '@graphpolaris/shared/lib/data-access/store/configSlice'; import { addError, setCurrentTheme } from '@graphpolaris/shared/lib/data-access/store/configSlice';
import { resetGraphQueryResults, queryingBackend } from '@graphpolaris/shared/lib/data-access/store/graphQueryResultSlice'; import { resetGraphQueryResults, queryingBackend } from '@graphpolaris/shared/lib/data-access/store/graphQueryResultSlice';
import { Query2BackendQuery, QueryMultiGraph } from '@graphpolaris/shared/lib/querybuilder'; import { Query2BackendQuery, QueryMultiGraph } from '@graphpolaris/shared/lib/querybuilder';
import { Navbar } from '../components/navbar/navbar'; import { Navbar } from '../components/navbar/navbar';
...@@ -24,6 +24,7 @@ import { InspectorPanel } from '@graphpolaris/shared/lib/inspector'; ...@@ -24,6 +24,7 @@ import { InspectorPanel } from '@graphpolaris/shared/lib/inspector';
import { SearchBar } from '@graphpolaris/shared/lib/sidebar/search/SearchBar'; import { SearchBar } from '@graphpolaris/shared/lib/sidebar/search/SearchBar';
import { Schema } from '@graphpolaris/shared/lib/schema/panel'; import { Schema } from '@graphpolaris/shared/lib/schema/panel';
import { InsightDialog } from '@graphpolaris/shared/lib/insight-sharing'; import { InsightDialog } from '@graphpolaris/shared/lib/insight-sharing';
import { ErrorBoundary } from '@graphpolaris/shared/lib/components/errorBoundary';
export type App = { export type App = {
load?: string; load?: string;
...@@ -93,21 +94,44 @@ export function App(props: App) { ...@@ -93,21 +94,44 @@ export function App(props: App) {
<Resizable divisorSize={3} horizontal={true} defaultProportion={0.33}> <Resizable divisorSize={3} horizontal={true} defaultProportion={0.33}>
{tab !== undefined ? ( {tab !== undefined ? (
<div className="flex flex-col w-full h-full"> <div className="flex flex-col w-full h-full">
{tab === 'Search' && <SearchBar onRemove={() => setTab(undefined)} />} {tab === 'Search' && (
{tab === 'Schema' && <Schema auth={authCheck} onRemove={() => setTab(undefined)} />} <ErrorBoundary
fallback={<div>Something went wrong</div>}
onError={() => dispatch(addError('Something went wrong while trying to load the search bar'))}
>
<SearchBar onRemove={() => setTab(undefined)} />
</ErrorBoundary>
)}
{tab === 'Schema' && (
<ErrorBoundary
fallback={<div>Something went wrong</div>}
onError={() => dispatch(addError('Something went wrong while trying to load the schema panel'))}
>
<Schema auth={authCheck} onRemove={() => setTab(undefined)} />
</ErrorBoundary>
)}
</div> </div>
) : null} ) : null}
<Resizable divisorSize={3} horizontal={false}> <Resizable divisorSize={3} horizontal={false}>
<VisualizationPanel <ErrorBoundary
fullSize={() => { fallback={<div>Something went wrong</div>}
// setVisFullSize(!visFullSize); onError={() => dispatch(addError('Something went wrong while trying to load the visualization panel'))}
// tab === undefined && setTab('Schema'); >
// tab !== undefined && setTab(undefined); <VisualizationPanel
}} fullSize={() => {
/> // setVisFullSize(!visFullSize);
// tab === undefined && setTab('Schema');
<QueryBuilder onRunQuery={runQuery} /> // tab !== undefined && setTab(undefined);
}}
/>
</ErrorBoundary>
<ErrorBoundary
fallback={<div>Something went wrong</div>}
onError={() => dispatch(addError('Something went wrong while trying to load the query builder'))}
>
<QueryBuilder onRunQuery={runQuery} />
</ErrorBoundary>
</Resizable> </Resizable>
</Resizable> </Resizable>
<InspectorPanel /> <InspectorPanel />
......
...@@ -16,15 +16,15 @@ import GpLogo from './gp-logo'; ...@@ -16,15 +16,15 @@ import GpLogo from './gp-logo';
import { Popover, PopoverContent, PopoverTrigger } from '@graphpolaris/shared/lib/components/layout/Popover'; import { Popover, PopoverContent, PopoverTrigger } from '@graphpolaris/shared/lib/components/layout/Popover';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { Dialog, DialogContent, DialogTrigger } from '@graphpolaris/shared/lib/components/layout/Dialog'; import { Dialog, DialogContent, DialogTrigger } from '@graphpolaris/shared/lib/components/layout/Dialog';
import { UserManagementContent } from '@graphpolaris/shared/lib/components/userManagementContent/UserManagementContent'; import { UserManagementContent } from '@graphpolaris/shared/lib/components/panels/userManagementContent/UserManagementContent';
import { addInfo } from '@graphpolaris/shared/lib/data-access/store/configSlice'; import { addInfo } from '@graphpolaris/shared/lib/data-access/store/configSlice';
import { showManagePermissions, showSharableExploration } from 'config';
export const Navbar = () => { export const Navbar = () => {
const dropdownRef = useRef<HTMLDivElement>(null); const dropdownRef = useRef<HTMLDivElement>(null);
const auth = useAuth(); const auth = useAuth();
const authCache = useAuthorizationCache(); const authCache = useAuthorizationCache();
const [menuOpen, setMenuOpen] = useState(false); const [menuOpen, setMenuOpen] = useState(false);
const [reportingOpen, setReportingOpen] = useState(false);
const dispatch = useDispatch(); const dispatch = useDispatch();
const buildInfo = import.meta.env.GRAPHPOLARIS_VERSION; const buildInfo = import.meta.env.GRAPHPOLARIS_VERSION;
...@@ -97,12 +97,14 @@ export const Navbar = () => { ...@@ -97,12 +97,14 @@ export const Navbar = () => {
</div> </div>
{authCache.authorized ? ( {authCache.authorized ? (
<> <>
<DropdownItem {showSharableExploration() && (
value="Share" <DropdownItem
onClick={() => { value="Share"
auth.newShareRoom(); onClick={() => {
}} auth.newShareRoom();
/> }}
/>
)}
<DropdownItem value="Settings" onClick={() => {}} /> <DropdownItem value="Settings" onClick={() => {}} />
<DropdownItem value="Log out" onClick={() => {}} /> <DropdownItem value="Log out" onClick={() => {}} />
</> </>
...@@ -119,7 +121,7 @@ export const Navbar = () => { ...@@ -119,7 +121,7 @@ export const Navbar = () => {
<div className="p-2 border-t"> <div className="p-2 border-t">
<h3 className="text-xs">Version: {buildInfo}</h3> <h3 className="text-xs">Version: {buildInfo}</h3>
</div> </div>
{writeAllowed && ( {showManagePermissions() && writeAllowed && (
<> <>
<Dialog> <Dialog>
<DialogTrigger className="ml-2 text-sm hover:bg-secondary-200">Manage Viewers Permission</DialogTrigger> <DialogTrigger className="ml-2 text-sm hover:bg-secondary-200">Manage Viewers Permission</DialogTrigger>
......
// Safely retrieve environment variable values with a default fallback
const getEnvVariable = (key: string, defaultValue: string = 'false'): string => {
return import.meta.env[key] ?? defaultValue;
};
// Check if the environment is production
const isProduction = (): boolean => {
return getEnvVariable('GRAPHPOLARIS_VERSION', 'dev') === 'prod';
};
// Check if the Manage Permissions feature is enabled
const showManagePermissions = (): boolean => {
return !isProduction() || (isProduction() && getEnvVariable('WIP_VIEWER_PERMISSIONS') === 'false');
};
// Check if the Insight Sharing feature is enabled
const showInsightSharing = (): boolean => {
return !isProduction() || (isProduction() && getEnvVariable('WIP_INSIGHT_SHARING') === 'false');
};
// Check if the Insight Sharing feature is enabled
const showSharableExploration = (): boolean => {
return !isProduction() || (isProduction() && getEnvVariable('WIP_SHARABLE_EXPLORATION') === 'false');
};
// Utility to check if a specific visualization is released based on environment variables
const isVisualizationReleased = (visualizationName: string): boolean => {
const visualizationFlag = getEnvVariable(`WIP_${visualizationName.toUpperCase()}`, 'false');
return !isProduction() || (isProduction() && visualizationFlag === 'false');
};
export { isProduction, showManagePermissions, showInsightSharing, showSharableExploration, isVisualizationReleased };
export * from './colors'; export * from './colors';
export * from './featureFlags';
...@@ -143,7 +143,7 @@ export const Input = (props: InputProps) => { ...@@ -143,7 +143,7 @@ export const Input = (props: InputProps) => {
switch (props.type) { switch (props.type) {
case 'slider': case 'slider':
return <SliderInput {...(props as SliderProps)} />; return <SliderInput {...(props as SliderProps)} />;
case 'text' || 'password': case 'text':
return <TextInput {...(props as TextProps)} />; return <TextInput {...(props as TextProps)} />;
case 'checkbox': case 'checkbox':
return <CheckboxInput {...(props as CheckboxProps)} />; return <CheckboxInput {...(props as CheckboxProps)} />;
......
// Dialog.stories.tsx
/* eslint-disable react-hooks/rules-of-hooks */
import React, { useState } from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import { Dialog, DialogTrigger, DialogContent, DialogHeading, DialogDescription, DialogClose } from './Dialog';
const metaDialog: Meta<typeof Dialog> = {
component: Dialog,
title: 'Components/Layout/Dialog',
};
export default metaDialog;
type Story = StoryObj<typeof Dialog>;
export const mainStory: Story = {
render: (args) => {
const [isOpen, setIsOpen] = useState(false);
const handleToggle = () => setIsOpen(!isOpen);
return (
<Dialog {...args} open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<button onClick={handleToggle} className="px-4 py-2 bg-secondary-200 text-white rounded">
Open Dialog
</button>
</DialogTrigger>
<DialogContent>
<DialogHeading>Dialog Title</DialogHeading>
<DialogDescription>This is a description inside the dialog.</DialogDescription>
<DialogClose>Close</DialogClose>
</DialogContent>
</Dialog>
);
},
args: {
initialOpen: false,
onOpenChange: (open: boolean) => console.log(`Dialog is ${open ? 'open' : 'closed'}`),
},
};
// Panel.stories.tsx
/* eslint-disable react-hooks/rules-of-hooks */
import React from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import { Panel } from './Panel';
const metaPanel: Meta<typeof Panel> = {
component: Panel,
title: 'Components/Layout/Panel',
};
export default metaPanel;
type Story = StoryObj<typeof Panel>;
export const mainStory: Story = {
render: (args) => (
<Panel {...args}>
<p>This is the content inside the panel.</p>
</Panel>
),
args: {
title: 'Panel Title',
tooltips: <span>Some tooltip</span>,
},
};
...@@ -7,7 +7,7 @@ import { Popover, PopoverTrigger, PopoverContent, PopoverHeading, PopoverDescrip ...@@ -7,7 +7,7 @@ import { Popover, PopoverTrigger, PopoverContent, PopoverHeading, PopoverDescrip
import { Icon } from '../icon'; import { Icon } from '../icon';
const metaPopover: Meta<typeof Popover> = { const metaPopover: Meta<typeof Popover> = {
component: Popover, component: Popover,
title: 'Components/Popover', title: 'Components/Layout/Popover',
}; };
export default metaPopover; export default metaPopover;
......
import React, { useState } from 'react';
import type { Meta, StoryObj } from '@storybook/react'; import type { Meta, StoryObj } from '@storybook/react';
import { useState } from 'react';
import { Pagination } from '.'; import { Pagination } from '.';
const metaPagination: Meta<typeof Pagination> = { const metaPagination: Meta<typeof Pagination> = {
......
...@@ -130,7 +130,7 @@ export const Pill = React.memo((props: PillI) => { ...@@ -130,7 +130,7 @@ export const Pill = React.memo((props: PillI) => {
> >
{props.children} {props.children}
</div> </div>
<div className="absolute z-50 pointer-events-auto">{props.handles}</div> <div className="absolute z-50 pointer-events-auto bg-black">{props.handles}</div>
</div> </div>
); );
}); });
......
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import type { RootState } from './store';
import { AllLayoutAlgorithms, Layouts } from '@graphpolaris/shared/lib/graph-layout'; import { AllLayoutAlgorithms, Layouts } from '@graphpolaris/shared/lib/graph-layout';
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { SchemaFromBackend, SchemaGraph, SchemaGraphInference, SchemaGraphology, SchemaGraphStats } from '../../schema';
import { SchemaViewState } from '../../schema/panel/Schema';
import { SchemaUtils } from '../../schema/schema-utils'; import { SchemaUtils } from '../../schema/schema-utils';
import { SchemaGraphStats, SchemaFromBackend, SchemaGraph, SchemaGraphology, SchemaGraphInference } from '../../schema'; import type { RootState } from './store';
/**************************************************************** */ /**************************************************************** */
...@@ -13,6 +14,7 @@ export type SchemaSettings = { ...@@ -13,6 +14,7 @@ export type SchemaSettings = {
layoutName: AllLayoutAlgorithms; layoutName: AllLayoutAlgorithms;
animatedEdges: boolean; animatedEdges: boolean;
showMinimap: boolean; showMinimap: boolean;
schemaViewState: SchemaViewState;
}; };
type schemaSliceI = { type schemaSliceI = {
...@@ -42,9 +44,12 @@ export const initialState: schemaSliceI = { ...@@ -42,9 +44,12 @@ export const initialState: schemaSliceI = {
// layoutName: 'Cytoscape_fcose', // layoutName: 'Cytoscape_fcose',
settings: { settings: {
connectionType: 'connection', connectionType: 'connection',
layoutName: Layouts.DAGRE, // layoutName: Layouts.DAGRE,
layoutName: Layouts.LISTINTERSECTED,
animatedEdges: false, animatedEdges: false,
showMinimap: true, showMinimap: true,
// schemaViewState: SchemaViewState.SchemaGraphS,
schemaViewState: SchemaViewState.SchemaListS,
}, },
}; };
export const schemaSlice = createSlice({ export const schemaSlice = createSlice({
......
...@@ -3,10 +3,37 @@ import Graph from 'graphology'; ...@@ -3,10 +3,37 @@ import Graph from 'graphology';
import { Attributes } from 'graphology-types'; import { Attributes } from 'graphology-types';
import { Layout } from './layout'; import { Layout } from './layout';
import { ILayoutFactory } from './layout-creator-usecase'; import { ILayoutFactory } from './layout-creator-usecase';
import { CytoscapeLayoutAlgorithms, LayoutAlgorithm } from './types'; import { LayoutAlgorithm } from './types';
export type CytoscapeProvider = 'Cytoscape'; export type CytoscapeProvider = 'Cytoscape';
export type CytoscapeLayoutAlgorithms =
| 'Cytoscape_klay'
| 'Cytoscape_dagre'
| 'Cytoscape_elk'
| 'Cytoscape_fcose'
| 'Cytoscape_cose-bilkent'
| 'Cytoscape_cise'
| 'Cytoscape_cose'
| 'Cytoscape_grid'
| 'Cytoscape_circle'
| 'Cytoscape_concentric'
| 'Cytoscape_breadthfirst';
export enum CytoscapeLayouts {
KLAY = 'Cytoscape_klay',
DAGRE = 'Cytoscape_dagre',
ELK = 'Cytoscape_elk',
FCOSE = 'Cytoscape_fcose',
COSE_BILKENT = 'Cytoscape_cose-bilkent',
CISE = 'Cytoscape_cise',
GRID = 'Cytoscape_grid',
COSE = 'Cytoscape_cose',
CIRCLE = 'Cytoscape_circle',
CONCENTRIC = 'Cytoscape_concentric',
BREATHFIRST = 'Cytoscape_breadthfirst',
}
type CytoNode = { type CytoNode = {
data: { data: {
id: string; id: string;
...@@ -20,8 +47,19 @@ type CytoNode = { ...@@ -20,8 +47,19 @@ type CytoNode = {
label?: string; label?: string;
count?: number; count?: number;
color?: string; color?: string;
sbgnbbox?: {
x: number;
y: number;
w: number;
h: number;
};
}; };
}; };
const DEFAULTWIDTH = 120;
const DEFAULTHEIGHT = 60;
/** /**
* This is the Cytoscape Factory * This is the Cytoscape Factory
*/ */
...@@ -116,6 +154,13 @@ export abstract class CytoscapeLayout extends Layout<CytoscapeProvider> { ...@@ -116,6 +154,13 @@ export abstract class CytoscapeLayout extends Layout<CytoscapeProvider> {
label: 'start', label: 'start',
count: 50, count: 50,
color: 'green', color: 'green',
sbgnbbox: {
x: 0,
y: 0,
w: DEFAULTWIDTH,
h: DEFAULTHEIGHT,
},
}, },
}); });
}); });
...@@ -167,7 +212,7 @@ export abstract class CytoscapeLayout extends Layout<CytoscapeProvider> { ...@@ -167,7 +212,7 @@ export abstract class CytoscapeLayout extends Layout<CytoscapeProvider> {
this.cytoscapeInstance = cytoscape({ this.cytoscapeInstance = cytoscape({
elements: cytonodes, elements: cytonodes,
headless: true, headless: true,
styleEnabled: false, styleEnabled: true,
}); });
return this.cytoscapeInstance; return this.cytoscapeInstance;
...@@ -284,23 +329,49 @@ class CytoscapeElk extends CytoscapeLayout { ...@@ -284,23 +329,49 @@ class CytoscapeElk extends CytoscapeLayout {
const cy = this.cytoscapeInstance; const cy = this.cytoscapeInstance;
if (!cy) return; if (!cy) return;
// options here https://github.com/cytoscape/cytoscape.js-elk cy.style().fromJson([
{
selector: 'node',
style: {
shape: 'rectangle',
width: function (n: any) {
return n['_private'].data.sbgnbbox?.w || DEFAULTWIDTH;
},
height: function (n: any) {
return n['_private'].data.sbgnbbox?.h || DEFAULTHEIGHT;
},
},
},
{
selector: 'edge',
style: {
opacity: 0.5,
},
},
]);
// options here https://github.com/cytoscape/cytoscape.js-elk
const layout = cy.layout({ const layout = cy.layout({
...this.defaultLayoutSettings, ...this.defaultLayoutSettings,
boundingBox: boundingBox, // boundingBox: boundingBox,
name: 'elk', name: 'elk',
fit: true, // nodeDimensionsIncludeLabels: true,
ranker: 'longest-path', // fit: true,
animate: false, // ranker: 'longest-path',
padding: 30, // animate: false,
// padding: 30,
elk: { elk: {
zoomToFit: true, // zoomToFit: true,
algorithm: 'layered', algorithm: 'box',
separateConnectedComponents: false, // separateConnectedComponents: false,
// direction: 'DOWN',
}, },
} as any); } as any);
layout.run(); const layouts = layout.run();
console.log('layouts', layouts);
this.updateNodePositions(); this.updateNodePositions();
} }
...@@ -332,7 +403,7 @@ class CytoscapeDagre extends CytoscapeLayout { ...@@ -332,7 +403,7 @@ class CytoscapeDagre extends CytoscapeLayout {
name: 'dagre', name: 'dagre',
// acyclicer: 'greedy', // acyclicer: 'greedy',
ranker: 'longest-path', ranker: 'longest-path',
spacingFactor: 0.7, spacingFactor: 0.95,
} as any); } as any);
layout.run(); layout.run();
......
...@@ -6,10 +6,25 @@ import noverlap from 'graphology-layout-noverlap'; ...@@ -6,10 +6,25 @@ import noverlap from 'graphology-layout-noverlap';
import { Attributes } from 'graphology-types'; import { Attributes } from 'graphology-types';
import { Layout } from './layout'; import { Layout } from './layout';
import { ILayoutFactory } from './layout-creator-usecase'; import { ILayoutFactory } from './layout-creator-usecase';
import { GraphologyLayoutAlgorithms, LayoutAlgorithm } from './types'; import { LayoutAlgorithm } from './types';
export type GraphologyProvider = 'Graphology'; export type GraphologyProvider = 'Graphology';
export type GraphologyLayoutAlgorithms =
| `Graphology_circular`
| `Graphology_random`
| `Graphology_noverlap`
| `Graphology_forceAtlas2`
| `Graphology_forceAtlas2_webworker`;
export enum GraphologyLayouts {
RANDOM = 'Graphology_random',
CIRCULAR = 'Graphology_circular',
NOVERLAP = 'Graphology_noverlap',
FORCEATLAS2 = 'Graphology_forceAtlas2',
FORCEATLAS2WEBWORKER = 'Graphology_forceAtlas2_webworker',
}
/** /**
* This is the Graphology Constructor for the main layouts available at * This is the Graphology Constructor for the main layouts available at
* https://graphology.github.io/ * https://graphology.github.io/
...@@ -73,7 +88,7 @@ export class GraphologyCircular extends GraphologyLayout { ...@@ -73,7 +88,7 @@ export class GraphologyCircular extends GraphologyLayout {
super.layout(graph, boundingBox); super.layout(graph, boundingBox);
// To directly assign the positions to the nodes: // To directly assign the positions to the nodes:
circular.assign(graph, { circular.assign(graph, {
scale: graph.order * 2, scale: (graph.order * graph.order) / 10,
...this.defaultLayoutSettings, ...this.defaultLayoutSettings,
}); });
} }
...@@ -96,7 +111,7 @@ export class GraphologyRandom extends GraphologyLayout { ...@@ -96,7 +111,7 @@ export class GraphologyRandom extends GraphologyLayout {
// To directly assign the positions to the nodes: // To directly assign the positions to the nodes:
random.assign(graph, { random.assign(graph, {
scale: graph.order * 1.5, scale: (graph.order * graph.order) / 10,
...this.defaultLayoutSettings, ...this.defaultLayoutSettings,
center: 0, center: 0,
}); });
......
import { CytoscapeFactory } from './cytoscape-layouts'; import { CytoscapeFactory, CytoscapeLayoutAlgorithms } from './cytoscape-layouts';
import { GraphologyFactory } from './graphology-layouts'; import { GraphologyFactory, GraphologyLayoutAlgorithms } from './graphology-layouts';
import { AllLayoutAlgorithms, AlgorithmToLayoutProvider, GraphologyLayoutAlgorithms, CytoscapeLayoutAlgorithms } from './types'; import { ListLayoutAlgorithms, ListLayoutFactory } from './list-layouts';
import { AlgorithmToLayoutProvider, AllLayoutAlgorithms } from './types';
export interface ILayoutFactory<Algorithm extends AllLayoutAlgorithms> { export interface ILayoutFactory<Algorithm extends AllLayoutAlgorithms> {
createLayout: (Algorithm: Algorithm) => AlgorithmToLayoutProvider<Algorithm> | null; createLayout: (Algorithm: Algorithm) => AlgorithmToLayoutProvider<Algorithm> | null;
...@@ -12,16 +13,19 @@ export interface ILayoutFactory<Algorithm extends AllLayoutAlgorithms> { ...@@ -12,16 +13,19 @@ export interface ILayoutFactory<Algorithm extends AllLayoutAlgorithms> {
export class LayoutFactory implements ILayoutFactory<AllLayoutAlgorithms> { export class LayoutFactory implements ILayoutFactory<AllLayoutAlgorithms> {
private graphologyFactory = new GraphologyFactory(); private graphologyFactory = new GraphologyFactory();
private cytoscapeFactory = new CytoscapeFactory(); private cytoscapeFactory = new CytoscapeFactory();
private listlayoutFactory = new ListLayoutFactory();
private isSpecificAlgorithm<Algorithm extends AllLayoutAlgorithms>( private isSpecificAlgorithm<Algorithm extends AllLayoutAlgorithms>(
LayoutAlgorithm: AllLayoutAlgorithms, LayoutAlgorithm: AllLayoutAlgorithms,
startsWith: string startsWith: string,
): LayoutAlgorithm is Algorithm { ): LayoutAlgorithm is Algorithm {
return LayoutAlgorithm.startsWith(startsWith); return LayoutAlgorithm.startsWith(startsWith);
} }
// todo make this static // todo make this static
createLayout<Algorithm extends AllLayoutAlgorithms>(layoutAlgorithm: Algorithm): AlgorithmToLayoutProvider<Algorithm> { createLayout<Algorithm extends AllLayoutAlgorithms = AllLayoutAlgorithms>(
layoutAlgorithm: Algorithm,
): AlgorithmToLayoutProvider<Algorithm> {
if (this.isSpecificAlgorithm<GraphologyLayoutAlgorithms>(layoutAlgorithm, 'Graphology')) { if (this.isSpecificAlgorithm<GraphologyLayoutAlgorithms>(layoutAlgorithm, 'Graphology')) {
return this.graphologyFactory.createLayout(layoutAlgorithm) as AlgorithmToLayoutProvider<Algorithm>; return this.graphologyFactory.createLayout(layoutAlgorithm) as AlgorithmToLayoutProvider<Algorithm>;
} }
...@@ -30,6 +34,10 @@ export class LayoutFactory implements ILayoutFactory<AllLayoutAlgorithms> { ...@@ -30,6 +34,10 @@ export class LayoutFactory implements ILayoutFactory<AllLayoutAlgorithms> {
return this.cytoscapeFactory.createLayout(layoutAlgorithm) as AlgorithmToLayoutProvider<Algorithm>; return this.cytoscapeFactory.createLayout(layoutAlgorithm) as AlgorithmToLayoutProvider<Algorithm>;
} }
if (this.isSpecificAlgorithm<ListLayoutAlgorithms>(layoutAlgorithm, 'List')) {
return this.listlayoutFactory.createLayout(layoutAlgorithm) as AlgorithmToLayoutProvider<Algorithm>;
}
throw Error('Invalid layout algorithm ' + layoutAlgorithm); throw Error('Invalid layout algorithm ' + layoutAlgorithm);
} }
} }
...@@ -23,6 +23,10 @@ export abstract class Layout<provider extends Providers> { ...@@ -23,6 +23,10 @@ export abstract class Layout<provider extends Providers> {
if (boundingBox !== undefined) { if (boundingBox !== undefined) {
this.boundingBox = boundingBox; this.boundingBox = boundingBox;
if (this.verbose) {
console.log(`Setting bounding box to ${JSON.stringify(this.boundingBox)}`);
}
} }
if (this.verbose) { if (this.verbose) {
......
import Graph from 'graphology';
import { Attributes } from 'graphology-types';
import { Layout } from './layout';
import { ILayoutFactory } from './layout-creator-usecase';
import { LayoutAlgorithm } from './types';
export type ListLayoutProvider = 'ListLayout';
export type ListLayoutAlgorithms = 'ListLayout_intersected' | 'ListLayout_nodesfirst' | 'ListLayout_edgesfirst';
export enum ListLayouts {
LISTINTERSECTED = 'ListLayout_intersected',
LISTNODEFIRST = 'ListLayout_nodesfirst',
LISTEDGEFIRST = 'ListLayout_edgesfirst',
}
export class ListLayoutFactory implements ILayoutFactory<ListLayoutAlgorithms> {
createLayout(layoutAlgorithm: ListLayoutAlgorithms): ListLayout | null {
switch (layoutAlgorithm) {
case 'ListLayout_intersected':
return new ListIntersectedLayout();
case 'ListLayout_nodesfirst':
return new ListNodesFirstLayout();
case 'ListLayout_edgesfirst':
return new ListEdgesFirstLayout();
default:
return null;
}
}
}
const Y_OFFSET = 50;
const X_RELATION_OFFSET = 50;
export abstract class ListLayout extends Layout<ListLayoutProvider> {
protected defaultLayoutSettings = {
dimensions: ['x', 'y'],
center: 0.5,
};
constructor(public override algorithm: LayoutAlgorithm<ListLayoutProvider>) {
super('ListLayout', algorithm);
}
/**
* Retrieves the position of a node in the graph layout.
* @param nodeId - The ID of the node.
* @returns The position of the node as an object with `x` and `y` coordinates.
* @throws Error if the node is not found in the current graph.
*/
public getNodePosition(nodeId: string) {
if (this.graph === null) {
throw new Error('The graph is not set.');
}
// console.log('Getting position for node:', nodeId, this.graph.getNodeAttributes(nodeId));
return this.graph.getNodeAttributes(nodeId);
}
}
/**
* This is a ConcreteProduct
*/
export class ListNodesFirstLayout extends ListLayout {
constructor() {
super('ListLayout_nodesfirst');
}
public override async layout(
graph: Graph<Attributes, Attributes, Attributes>,
boundingBox?: { x1: number; x2: number; y1: number; y2: number },
): Promise<void> {
super.layout(graph, boundingBox);
if (this.verbose) {
console.log('ListLayout_nodesfirst layouting now', boundingBox);
}
if (boundingBox === undefined) {
boundingBox = { x1: 0, x2: 1000, y1: 0, y2: 1000 };
}
const relationNodes = graph.nodes().filter((node) => node.startsWith('Relation'));
const entityNodes = graph.nodes().filter((node) => !relationNodes.includes(node));
let y = 0;
entityNodes.map((node, index) => {
y = index * Y_OFFSET;
graph.updateNodeAttribute(node, 'x', () => boundingBox.x1);
graph.updateNodeAttribute(node, 'y', () => y);
});
relationNodes.map((node, index) => {
const relationsY = y + Y_OFFSET + index * Y_OFFSET;
graph.updateNodeAttribute(node, 'x', () => boundingBox.x1);
graph.updateNodeAttribute(node, 'y', () => relationsY);
});
if (this.verbose) {
console.log(`ListLayout_nodesfirst layouting finished`);
}
}
}
/**
* This is a ConcreteProduct
*/
export class ListEdgesFirstLayout extends ListLayout {
constructor() {
super('ListLayout_edgesfirst');
}
public override async layout(
graph: Graph<Attributes, Attributes, Attributes>,
boundingBox?: { x1: number; x2: number; y1: number; y2: number },
): Promise<void> {
super.layout(graph, boundingBox);
if (this.verbose) {
console.log('ListLayout_edgesfirst layouting now', boundingBox);
}
if (boundingBox === undefined) {
boundingBox = { x1: 0, x2: 1000, y1: 0, y2: 1000 };
}
const relationNodes = graph.nodes().filter((node) => node.startsWith('Relation'));
const entityNodes = graph.nodes().filter((node) => !relationNodes.includes(node));
let y = 0;
relationNodes.map((node, index) => {
y = index * Y_OFFSET;
graph.updateNodeAttribute(node, 'x', () => boundingBox.x1);
graph.updateNodeAttribute(node, 'y', () => y);
});
entityNodes.map((node, index) => {
const relationsY = y + Y_OFFSET + index * Y_OFFSET;
graph.updateNodeAttribute(node, 'x', () => boundingBox.x1);
graph.updateNodeAttribute(node, 'y', () => relationsY);
});
if (this.verbose) {
console.log(`ListLayout_edgesfirst layouting finished`);
}
}
}
/**
* This is a ConcreteProduct
*/
export class ListIntersectedLayout extends ListLayout {
constructor() {
super('ListLayout_intersected');
}
public override async layout(
graph: Graph<Attributes, Attributes, Attributes>,
boundingBox?: { x1: number; x2: number; y1: number; y2: number },
): Promise<void> {
super.layout(graph, boundingBox);
if (this.verbose) {
console.log('ListLayout_intersected layouting now', boundingBox);
}
if (boundingBox === undefined) {
boundingBox = { x1: 0, x2: 1000, y1: 0, y2: 1000 };
}
const relationNodes = graph.nodes().filter((node) => node.startsWith('Relation'));
const entityNodes = graph.nodes().filter((node) => !relationNodes.includes(node));
const graphAllNodes = graph.nodes();
const intersectedList: string[] = [];
entityNodes.forEach((node) => {
intersectedList.push(node);
graph.forEachNeighbor(node, (neighbor) => {
if (graphAllNodes.includes(neighbor)) {
graphAllNodes.splice(graphAllNodes.indexOf(neighbor), 1);
intersectedList.push(neighbor);
}
});
});
let y = 0;
intersectedList.map((node, index) => {
y = index * Y_OFFSET;
graph.updateNodeAttribute(node, 'x', () => {
if (node.startsWith('Relation')) {
return boundingBox.x1 + X_RELATION_OFFSET;
} else {
return boundingBox.x1;
}
});
graph.updateNodeAttribute(node, 'y', () => y);
});
if (this.verbose) {
console.log(`ListLayout_intersected layouting finished`);
}
}
}
import { CytoscapeLayout, CytoscapeProvider } from './cytoscape-layouts'; import { CytoscapeLayout, CytoscapeLayoutAlgorithms, CytoscapeLayouts, CytoscapeProvider } from './cytoscape-layouts';
import { GraphologyLayout, GraphologyProvider } from './graphology-layouts'; import { GraphologyLayout, GraphologyLayoutAlgorithms, GraphologyLayouts, GraphologyProvider } from './graphology-layouts';
import { ListLayout, ListLayoutAlgorithms, ListLayoutProvider, ListLayouts } from './list-layouts';
export type GraphologyLayoutAlgorithms = export type AllLayoutAlgorithms = GraphologyLayoutAlgorithms | CytoscapeLayoutAlgorithms | ListLayoutAlgorithms;
| `Graphology_circular`
| `Graphology_random`
| `Graphology_noverlap`
| `Graphology_forceAtlas2`
| `Graphology_forceAtlas2_webworker`;
export type CytoscapeLayoutAlgorithms = export type Providers = GraphologyProvider | CytoscapeProvider | ListLayoutProvider;
| 'Cytoscape_klay'
| 'Cytoscape_dagre'
| 'Cytoscape_elk'
| 'Cytoscape_fcose'
| 'Cytoscape_cose-bilkent'
| 'Cytoscape_cise'
| 'Cytoscape_cose'
| 'Cytoscape_grid'
| 'Cytoscape_circle'
| 'Cytoscape_concentric'
| 'Cytoscape_breadthfirst';
export type AllLayoutAlgorithms = GraphologyLayoutAlgorithms | CytoscapeLayoutAlgorithms;
export type Providers = GraphologyProvider | CytoscapeProvider;
export type LayoutAlgorithm<Provider extends Providers> = `${Provider}_${string}`; export type LayoutAlgorithm<Provider extends Providers> = `${Provider}_${string}`;
export type AlgorithmToLayoutProvider<Algorithm extends AllLayoutAlgorithms> = Algorithm extends GraphologyLayoutAlgorithms export type AlgorithmToLayoutProvider<Algorithm extends AllLayoutAlgorithms = AllLayoutAlgorithms> =
? GraphologyLayout Algorithm extends GraphologyLayoutAlgorithms
: Algorithm extends CytoscapeLayoutAlgorithms ? GraphologyLayout
? CytoscapeLayout : Algorithm extends CytoscapeLayoutAlgorithms
: CytoscapeLayout | GraphologyLayout; ? CytoscapeLayout
: ListLayout;
export const Layouts = {
...GraphologyLayouts,
...CytoscapeLayouts,
...ListLayouts,
} as const;
export enum Layouts { export type LayoutTypes = (typeof Layouts)[keyof typeof Layouts];
KLAY = 'Cytoscape_klay',
DAGRE = 'Cytoscape_dagre',
ELK = 'Cytoscape_elk',
FCOSE = 'Cytoscape_fcose',
COSE_BILKENT = 'Cytoscape_cose-bilkent',
CISE = 'Cytoscape_cise',
GRID = 'Cytoscape_grid',
COSE = 'Cytoscape_cose',
CIRCLE = 'Cytoscape_circle',
CONCENTRIC = 'Cytoscape_concentric',
BREATHFIRST = 'Cytoscape_breadthfirst',
RANDOM = 'Graphology_random',
CIRCULAR = 'Graphology_circular',
NOVERLAP = 'Graphology_noverlap',
FORCEATLAS2 = 'Graphology_forceAtlas2',
FORCEATLAS2WEBWORKER = 'Graphology_forceAtlas2_webworker',
}
...@@ -18,7 +18,7 @@ export function InspectorPanel(props: { children?: React.ReactNode }) { ...@@ -18,7 +18,7 @@ export function InspectorPanel(props: { children?: React.ReactNode }) {
const { activeVisualizationIndex } = useVisualization(); const { activeVisualizationIndex } = useVisualization();
const inspector = useMemo(() => { const inspector = useMemo(() => {
//if (selection) return <SelectionConfig />; if (selection) return <SelectionConfig />;
// if (!focus) return <ConnectionInspector />; // if (!focus) return <ConnectionInspector />;
// if (activeVisualizationIndex !== -1) return <ConnectionInspector />; // if (activeVisualizationIndex !== -1) return <ConnectionInspector />;
return <VisualizationSettings />; return <VisualizationSettings />;
......