Skip to content

Commit

Permalink
Merge pull request #264 from MetaCell/feature/graph_update
Browse files Browse the repository at this point in the history
Re-aligning graph styling with sckan explorer
  • Loading branch information
ddelpiano authored Apr 15, 2024
2 parents 6343df9 + ed10f43 commit a002cd6
Show file tree
Hide file tree
Showing 12 changed files with 956 additions and 338 deletions.
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

0 comments on commit a002cd6

Please sign in to comment.