Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Re-aligning graph styling with sckan explorer #264

Merged
merged 4 commits into from
Apr 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions frontend/src/apiclient/backend/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6734,6 +6734,3 @@ export class MetacellAuthApi extends BaseAPI {
return MetacellAuthApiFp(this.configuration).metacellAuthLogoutRetrieve(options).then((request) => request(this.axios, this.basePath));
}
}



Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { AbstractReactFactory } from '@projectstorm/react-canvas-core';
import {NodeTypes} from "../GraphDiagram";
import {AbstractReactFactory, GenerateWidgetEvent} from '@projectstorm/react-canvas-core';
import {CustomNodeModel} from "../Models/CustomNodeModel";
import React from "react";
import {Fragment} from "react";
import {OriginNodeWidget} from "../Widgets/OriginNodeWidget";
import {ViaNodeWidget} from "../Widgets/ViaNodeWidget";
import {DestinationNodeWidget} from "../Widgets/DestinationNodeWidget";
import {DiagramEngine, NodeModel, NodeModelGenerics} from "@projectstorm/react-diagrams";
import {NodeTypes} from "../GraphDiagram";

export class CustomNodeFactory extends AbstractReactFactory {
export class CustomNodeFactory extends AbstractReactFactory<NodeModel<NodeModelGenerics>, DiagramEngine> {
constructor() {
super('custom');
}
Expand All @@ -15,8 +16,7 @@ export class CustomNodeFactory extends AbstractReactFactory {
return new CustomNodeModel(NodeTypes.Origin, '');
}

// @ts-ignore
generateReactWidget(event: any) {
generateReactWidget(event: GenerateWidgetEvent<CustomNodeModel>) {
const { customType } = event.model;

switch (customType) {
Expand All @@ -27,7 +27,7 @@ export class CustomNodeFactory extends AbstractReactFactory {
case NodeTypes.Destination:
return <DestinationNodeWidget engine={this.engine} model={event.model} />;
default:
return null;
return <Fragment></Fragment>;
}
}
}
124 changes: 79 additions & 45 deletions frontend/src/components/ProofingTab/GraphDiagram/GraphDiagram.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import React, {useEffect, useRef, useState} from "react";
import {
AnatomicalEntity,
DestinationSerializerDetails, TypeB60Enum, TypeC11Enum,
ViaSerializerDetails
} from "../../../apiclient/backend";
import InfoMenu from "./InfoMenu";
import NavigationMenu from "./NavigationMenu";
import createEngine, {
BasePositionModelOptions,
DefaultLinkModel,
DiagramModel,
} from '@projectstorm/react-diagrams';
import {CanvasWidget} from '@projectstorm/react-canvas-core';
import {CustomNodeModel} from "./Models/CustomNodeModel";
import {CustomNodeFactory} from "./Factories/CustomNodeFactory";
import {getURI, getName} from "../../../helpers/anatomicalEntityHelper";
import {
AnatomicalEntity,
DestinationSerializerDetails,
TypeB60Enum,
TypeC11Enum,
ViaSerializerDetails
} from "../../../apiclient/backend";


export enum NodeTypes {
Expand All @@ -22,6 +24,13 @@ export enum NodeTypes {
Destination = 'Destination'
}

export interface CustomNodeOptions extends BasePositionModelOptions {
forward_connection: any[];
to?: Array<{ name: string; type: string }>;
from?: Array<{ name: string; type: string }>;
anatomicalType?: string;
}

const ViaTypeMapping: Record<TypeB60Enum, string> = {
[TypeB60Enum.Axon]: 'Axon',
[TypeB60Enum.Dendrite]: 'Dendrite'
Expand All @@ -37,21 +46,17 @@ interface GraphDiagramProps {
origins: AnatomicalEntity[] | undefined;
vias: ViaSerializerDetails[] | undefined;
destinations: DestinationSerializerDetails[] | undefined;
forward_connection?: any[] | undefined;
}

function getExternalID(url: string) {
const parts = url.split('/');
return parts[parts.length - 1].replace('_', ':');
}

function getOrderId(layerId: string, entity: AnatomicalEntity) {
function getId(layerId: string, entity: AnatomicalEntity) {
return layerId + entity.id.toString();
}

function findNodeForEntity(entity: AnatomicalEntity, nodeMap: Map<string, CustomNodeModel>, maxLayerIndex: number) {
for (let layerIndex = 0; layerIndex <= maxLayerIndex; layerIndex++) {
let layerId = layerIndex === 0 ? NodeTypes.Origin : NodeTypes.Via + layerIndex
let id = getOrderId(layerId, entity);
const layerId = layerIndex === 0 ? NodeTypes.Origin : NodeTypes.Via + layerIndex
const id = getId(layerId, entity);
if (nodeMap.has(id)) {
return nodeMap.get(id);
}
Expand All @@ -66,6 +71,7 @@ const createLink = (sourceNode: CustomNodeModel, targetNode: CustomNodeModel, so
const link = new DefaultLinkModel();
link.setSourcePort(sourcePort);
link.setTargetPort(targetPort);
link.getOptions().curvyness = 0
return link;
}
return null;
Expand All @@ -75,24 +81,29 @@ const processData = (
origins: AnatomicalEntity[] | undefined,
vias: ViaSerializerDetails[] | undefined,
destinations: DestinationSerializerDetails[] | undefined,
forward_connection: any[],
): { nodes: CustomNodeModel[], links: DefaultLinkModel[] } => {
const nodes: CustomNodeModel[] = [];
const links: DefaultLinkModel[] = [];

const nodeMap = new Map<string, CustomNodeModel>();

const yStart = 100
const yIncrement = 200; // Vertical spacing
const xIncrement = 200; // Horizontal spacing
const yStart = 50
const yIncrement = 250; // Vertical spacing
const xIncrement = 250; // Horizontal spacing
let xOrigin = 100

origins?.forEach(origin => {
const id = getOrderId(NodeTypes.Origin, origin)
const id = getId(NodeTypes.Origin, origin)
const name = origin.simple_entity !== null ? origin.simple_entity.name : origin.region_layer.region.name + '(' + origin.region_layer.layer.name + ')';
const ontology_uri = origin.simple_entity !== null ? origin.simple_entity.ontology_uri : origin.region_layer.region.ontology_uri + ', ' + origin.region_layer.layer.ontology_uri;
const fws: never[] = []
const originNode = new CustomNodeModel(
NodeTypes.Origin,
getName(origin),
getExternalID(getURI(origin)),
name,
ontology_uri,
{
forward_connection: fws,
to: [],
}
);
Expand All @@ -105,15 +116,19 @@ const processData = (

vias?.forEach((via) => {
const layerIndex = via.order + 1
let xVia = 100
let xVia = 120
let yVia = layerIndex * yIncrement + yStart;
via.anatomical_entities.forEach(entity => {
const id = getOrderId(NodeTypes.Via + layerIndex, entity)
const id = getId(NodeTypes.Via + layerIndex, entity)
const name = entity.simple_entity !== null ? entity.simple_entity.name : entity.region_layer.region.name + '(' + entity.region_layer.layer.name + ')';
const ontology_uri = entity.simple_entity !== null ? entity.simple_entity.ontology_uri : entity.region_layer.region.ontology_uri + ', ' + entity.region_layer.layer.ontology_uri;
const fws: never[] = []
const viaNode = new CustomNodeModel(
NodeTypes.Via,
getName(entity),
getExternalID(getURI(entity)),
name,
ontology_uri,
{
forward_connection: fws,
from: [],
to: [],
anatomicalType: via?.type ? ViaTypeMapping[via.type] : ''
Expand All @@ -130,10 +145,10 @@ const processData = (
const link = createLink(sourceNode, viaNode, 'out', 'in');
if (link) {
links.push(link);
// @ts-ignore
sourceNode.getOptions()["to"]?.push({name: viaNode.name, type: NodeTypes.Via})
// @ts-ignore
viaNode.getOptions()["from"]?.push({name: sourceNode.name, type: sourceNode.getCustomType()})
const sourceOptions = sourceNode.getOptions() as CustomNodeOptions;
const viaOptions = viaNode.getOptions() as CustomNodeOptions;
sourceOptions.to?.push({name: viaNode.name, type: NodeTypes.Via});
viaOptions.from?.push({name: sourceNode.name, type: sourceNode.getCustomType()})
}
}
});
Expand All @@ -143,19 +158,29 @@ const processData = (


const yDestination = yIncrement * ((vias?.length || 1) + 1) + yStart
let xDestination = 100
let xDestination = 115


// Process Destinations
destinations?.forEach(destination => {
destination.anatomical_entities.forEach(entity => {
const name = entity.simple_entity !== null ? entity.simple_entity.name : entity.region_layer.region.name + '(' + entity.region_layer.layer.name + ')';
const ontology_uri = entity.simple_entity !== null ? entity.simple_entity.ontology_uri : entity.region_layer.region.ontology_uri + ', ' + entity.region_layer.layer.ontology_uri;
const fws = forward_connection.filter(single_fw => {
const origins = single_fw.origins.map((origin: { id: string } | string) => typeof origin === 'object' ? origin.id : origin);
if (origins.includes(entity.id)) {
return true;
}
return false;
});
const destinationNode = new CustomNodeModel(
NodeTypes.Destination,
getName(entity),
getExternalID(getURI(entity)),
name,
ontology_uri,
{
forward_connection: fws,
from: [],
anatomicalType: destination?.type ? DestinationTypeMapping[destination.type] : ''
anatomicalType: destination?.type ? DestinationTypeMapping[destination.type] : '',
}
);
destinationNode.setPosition(xDestination, yDestination);
Expand All @@ -168,13 +193,10 @@ const processData = (
const link = createLink(sourceNode, destinationNode, 'out', 'in');
if (link) {
links.push(link);
// @ts-ignore
sourceNode.getOptions()["to"]?.push({name: destinationNode.name, type: NodeTypes.Destination})
// @ts-ignore
destinationNode.getOptions()["from"]?.push({
name: sourceNode.name,
type: sourceNode.getCustomType()
})
const sourceOptions = sourceNode.getOptions() as CustomNodeOptions;
const destinationOptions = destinationNode.getOptions() as CustomNodeOptions;
sourceOptions.to?.push({name: destinationNode.name, type: NodeTypes.Destination});
destinationOptions.from?.push({name: sourceNode.name, type: sourceNode.getCustomType()})
}
}
});
Expand All @@ -184,35 +206,36 @@ const processData = (
return {nodes, links};
};

const GraphDiagram: React.FC<GraphDiagramProps> = ({origins, vias, destinations}) => {
const GraphDiagram: React.FC<GraphDiagramProps> = ({origins, vias, destinations, forward_connection = []}) => {
const [engine] = useState(() => createEngine());
const [modelUpdated, setModelUpdated] = useState(false)
const [modelFitted, setModelFitted] = useState(false)
const containerRef = useRef<HTMLDivElement>(null);

// This effect runs once to set up the engine
useEffect(() => {
engine.getNodeFactories().registerFactory(new CustomNodeFactory() as any);
engine.getNodeFactories().registerFactory(new CustomNodeFactory());
}, [engine]);


// This effect runs whenever origins, vias, or destinations change
useEffect(() => {
const {nodes, links} = processData(origins, vias, destinations);
const {nodes, links} = processData(origins, vias, destinations, forward_connection);

const model = new DiagramModel();
model.addAll(...nodes, ...links);

engine.setModel(model);
// engine.getModel().setLocked(true)
setModelUpdated(true)
}, [origins, vias, destinations, engine]);
}, [origins, vias, destinations, engine, forward_connection]);

// This effect prevents the default scroll and touchmove behavior
useEffect(() => {
const currentContainer = containerRef.current;

if (modelUpdated && currentContainer) {
const disableScroll = (event: any) => {
const disableScroll = (event: Event) => {
event.stopPropagation();
};

Expand All @@ -226,11 +249,22 @@ const GraphDiagram: React.FC<GraphDiagramProps> = ({origins, vias, destinations}
}
}, [modelUpdated]);

useEffect(() => {
if (modelUpdated && !modelFitted) {
// TODO: for unknown reason at the moment if I call zoomToFit too early breaks the graph
// To fix later in the next contract.
setTimeout(() => {
engine.zoomToFit();
}, 1000);
setModelFitted(true);
}
}, [modelUpdated, modelFitted, engine]);

return (
modelUpdated ? (
<div ref={containerRef} className={"graphContainer"}>
<NavigationMenu engine={engine}/>
<InfoMenu engine={engine}/>
<InfoMenu engine={engine} forwardConnection={true} />
<CanvasWidget className={"graphContainer"} engine={engine}/>
</div>)
: null
Expand Down
Loading