-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'main' of https://github.com/the-virtual-brain/tvb-ext-o…
- Loading branch information
Showing
17 changed files
with
562 additions
and
80 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} />; | ||
} | ||
} |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Oops, something went wrong.