Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
leon-k-martin committed Aug 21, 2024
2 parents 48d6532 + b7884b4 commit 763ce76
Show file tree
Hide file tree
Showing 17 changed files with 562 additions and 80 deletions.
53 changes: 53 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React, { useState } from 'react';
import '../style/layout.css';
import { GraphViewComponent } from './components/GaphView';
import InfoBoxComponent from './components/InfoBox';
import WorkspaceComponent from './components/Workspace';
import { ISelectedNodeType } from './components/interfaces/InfoBoxInterfaces';
import { IWorkspaceState } from './components/interfaces/WorkspaceInterfaces';

interface IAppProps {
fetchData: () => Promise<any>;
}

const App: React.FC<IAppProps> = () => {
const [selectedNode, setSelectedNode] = useState<ISelectedNodeType | null>(null);

const [workspace, setWorkspace] = useState<IWorkspaceState>({
model: null,
connectivity: null,
coupling: null,
noise: null,
integrationMethod: null
});

const addToWorkspace = (node: ISelectedNodeType) => {
setWorkspace(prevWorkspace => {
switch (node.type) {
case 'Neural Mass Model':
return { ...prevWorkspace, model: node };
case 'TheVirtualBrain':
if (node.label.includes('Noise')) {
return { ...prevWorkspace, noise: node };
} else {
return { ...prevWorkspace, connectivity: node };
}
case 'Coupling':
return { ...prevWorkspace, coupling: node };
case 'Integrator':
return { ...prevWorkspace, integrationMethod: node };
default:
return prevWorkspace; // No changes if the type doesn't match
}
});
};

return (
<div className="layout">
<InfoBoxComponent selectedNode={selectedNode} addToWorkspace={addToWorkspace} />
<WorkspaceComponent workspace={workspace} />
<GraphViewComponent setSelectedNode={setSelectedNode} />
</div>
);
};
export default App;
16 changes: 16 additions & 0 deletions src/AppWidget.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { ReactWidget } from '@jupyterlab/apputils';
import React from 'react';
import App from './App';

export class AppWidget extends ReactWidget {
fetchData: () => Promise<any>;
constructor(fetchData: () => Promise<any>) {
super();
this.addClass('tvbo-AppWidget');
this.fetchData = fetchData;
}

render(): React.ReactElement {
return <App fetchData={this.fetchData} />;
}
}
69 changes: 0 additions & 69 deletions src/OntologyWidget.tsx

This file was deleted.

173 changes: 173 additions & 0 deletions src/components/GaphView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import React, { useEffect, useState } from 'react';
import ForceGraph2D from 'react-force-graph-2d';
import { fetchNodeByLabel, fetchNodeConnections } from '../handler';
import { ISelectedNodeType } from './interfaces/InfoBoxInterfaces';
import { ILinkType, INodeType } from './interfaces/GraphViewInterfaces';
import { ITreeNode } from './interfaces/TreeViewInterfaces';
import TreeViewComponent from './TreeView';

interface IGraphViewProps {
setSelectedNode: (node: ISelectedNodeType) => void;
}

export const GraphViewComponent: React.FC<IGraphViewProps> = ({
setSelectedNode
}) => {
const [data, setData] = useState<{ nodes: INodeType[]; links: ILinkType[]; }>({ nodes: [], links: [] });
const [searchLabel, setSearchLabel] = useState<string>('');
const [treeData, setTreeData] = useState<ITreeNode | null>(null);

useEffect(() => {
const fetchAndSetData = async (label?: string) => {
try {
// Fetch data
const result = await fetchNodeByLabel(label || '');
setData(result);
} catch (error) {
console.error('Failed to fetch data:', error);
setData({ nodes: [], links: [] });
}
};

fetchAndSetData(searchLabel);
}, [searchLabel]);

const buildTree = (currentNode: INodeType): ITreeNode => {
const nodeMap = new Map<number, ITreeNode>();

// Initialize parents and children for all nodes in graph
data.nodes.forEach(node => {
nodeMap.set(node.id, {
id: node.id,
label: node.label,
type: node.type,
children: [],
parents: []
});
});
console.log(nodeMap);

const currentTreeNode = nodeMap.get(currentNode.id)!;

// Get parents and children
data.links.forEach(link => {
console.log('Link: ', link);
const sourceNode = nodeMap.get(link.source);
const targetNode = nodeMap.get(link.target);
console.log('Source Node: ', sourceNode);
console.log('Target Node: ', targetNode);

if (sourceNode && targetNode) {
if (link.target === currentNode.id) {
currentTreeNode.parents.push(sourceNode);
} else if (link.source === currentNode.id) {
currentTreeNode.children.push(targetNode);
}

// Handle cases where a child node is also connected to the parents
if (currentTreeNode.parents.includes(targetNode) && currentTreeNode.children.includes(sourceNode)) {
sourceNode.parents.push(targetNode);
targetNode.children.push(sourceNode);
}
}
});

return currentTreeNode;
};

const handleNodeClick = async (node: INodeType) => {
setSelectedNode({
label: node.label,
type: node.type,
definition: node.definition,
iri: node.iri,
childLinks: node.childLinks,
collapsed: false
});
console.log('Node clicked');

// Build the tree view for the clicked node
const tree = buildTree(node);
setTreeData(tree);

const connections = await fetchNodeConnections(node.label);

const nodesById = Object.fromEntries(
data.nodes.map(node => [node.id, node])
);

// link parent/children
data.nodes.forEach(n => {
n.collapsed = n.id !== node.id;
n.childLinks = [];
});

connections!.links.forEach(link => {
const sourceNode = nodesById[link.source];
if (sourceNode) {
sourceNode.childLinks!.push(link);
} else {
console.error(
`Node with id ${link.source} does not exist in nodesById`
);
}
});
data.nodes = Object.values(nodesById);
};

// Handle search
const handleSearch = () => {
if (searchLabel.trim()) {
setSearchLabel(searchLabel.trim());
}
};

// Handle Enter key press to trigger the search
const handleKeyPress = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
handleSearch();
}
};

return (
<div className="ontology-graph">
<TreeViewComponent treeData={treeData} />
<div className="search-bar">
<input
type="text"
value={searchLabel}
onChange={e => setSearchLabel(e.target.value)}
onKeyPress={handleKeyPress}
placeholder="Enter node label"
/>
<button onClick={handleSearch}>Search</button>
</div>
<div>
{data ? (
<ForceGraph2D
graphData={data}
onNodeClick={handleNodeClick}
linkCurvature={0.25}
nodeCanvasObject={(node, ctx, globalScale) => {
const label = node.label;
const fontSize = 12 / globalScale;
ctx.font = `${fontSize}px Sans-Serif`;
ctx.fillStyle = 'black';
ctx.textAlign = 'center';
ctx.textBaseline = 'top';

const xCoord = node.x as number;
const yCoord = node.y as number;
ctx.fillText(label, xCoord, yCoord + 5);
}}
nodeCanvasObjectMode={() => 'after'}
linkDirectionalArrowLength={3.5}
linkDirectionalArrowRelPos={1}
/>
) : (
<div>Loading...</div>
)}
</div>
</div>
);
};
62 changes: 62 additions & 0 deletions src/components/InfoBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React from 'react';
import { ISelectedNodeType } from './interfaces/InfoBoxInterfaces';

interface IInfoBoxProps {
selectedNode: {
label: string;
type: string;
definition: string;
iri: string;
} | null;
addToWorkspace: (node: ISelectedNodeType) => void;
}

const InfoBoxComponent: React.FC<IInfoBoxProps> = ({
selectedNode,
addToWorkspace
}) => {
// Valid types for adding objects to workspace
const validTypes = [
'Neural Mass Model',
'TheVirtualBrain', // TODO: change to actual type for connectivity, noise
'Coupling',
'Integrator'
];

const isAddable = selectedNode && validTypes.includes(selectedNode.type);

return (
<div className="info-box">
{selectedNode ? (
<div>
<h3>Node Information</h3>
<p>
<strong>Name:</strong> {selectedNode.label}
</p>
<p>
<strong>Type:</strong> {selectedNode.type}
</p>
<p>
<strong>Definition:</strong> {selectedNode.definition}
</p>
<p>
<strong>IRI:</strong>{' '}
<a href={selectedNode.iri} target="_blank" rel="noopener noreferrer">
{selectedNode.iri}
</a>
</p>
<button
onClick={() => addToWorkspace(selectedNode)}
disabled={!isAddable}
>
Add to Workspace
</button>
</div>
) : (
<p>Select a node to see its details here</p>
)}
</div>
);
};

export default InfoBoxComponent;
7 changes: 7 additions & 0 deletions src/components/SearchBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import React from 'react';

const SeachBarComponent: React.FC = () => {
return <div className="search-bar">Search Bar</div>;
};

export default SeachBarComponent;
Loading

0 comments on commit 763ce76

Please sign in to comment.