From b7884b4116629232bfff6523706f9c586f2a3bd2 Mon Sep 17 00:00:00 2001 From: romina1601 Date: Fri, 16 Aug 2024 18:58:27 +0300 Subject: [PATCH] VT-23. Add components for GraphView, TreeView, InfoBox and Workspace with some functionalities for each --- src/App.tsx | 59 +++--- src/OntologyWidget.tsx | 78 -------- src/components/GaphView.tsx | 173 ++++++++++++++++++ src/components/InfoBox.tsx | 59 +++++- src/components/TreeView.tsx | 37 +++- src/components/Workspace.tsx | 29 ++- .../interfaces/GraphViewInterfaces.tsx | 17 ++ .../interfaces/InfoBoxInterfaces.tsx | 10 + .../interfaces/TreeViewInterfaces.tsx | 7 + .../interfaces/WorkspaceInterfaces.tsx | 13 ++ src/handler.ts | 11 +- src/index.ts | 2 +- style/layout.css | 38 +++- tvb_ext_ontology/handlers.py | 13 +- 14 files changed, 427 insertions(+), 119 deletions(-) delete mode 100644 src/OntologyWidget.tsx create mode 100644 src/components/GaphView.tsx create mode 100644 src/components/interfaces/GraphViewInterfaces.tsx create mode 100644 src/components/interfaces/InfoBoxInterfaces.tsx create mode 100644 src/components/interfaces/TreeViewInterfaces.tsx create mode 100644 src/components/interfaces/WorkspaceInterfaces.tsx diff --git a/src/App.tsx b/src/App.tsx index 97e87a3..ab9caa1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,39 +1,52 @@ -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; import '../style/layout.css'; -import { OntologyWidgetComponent } from './OntologyWidget'; -import TreeViewComponent from './components/TreeView'; -import SeachBarComponent from './components/SearchBar'; +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; } -const App: React.FC = ({ fetchData }) => { - const [data, setData] = useState(null); +const App: React.FC = () => { + const [selectedNode, setSelectedNode] = useState(null); - useEffect(() => { - const fetchAndSetData = async () => { - try { - const result = await fetchData(); - setData(result); - console.log(data); - } catch (error) { - console.error('Failed to fetch data:', error); - } - }; + const [workspace, setWorkspace] = useState({ + model: null, + connectivity: null, + coupling: null, + noise: null, + integrationMethod: null + }); - fetchAndSetData(); - }, [fetchData]); + 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 (
- - - - - + + +
); }; diff --git a/src/OntologyWidget.tsx b/src/OntologyWidget.tsx deleted file mode 100644 index 1190464..0000000 --- a/src/OntologyWidget.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import ForceGraph2D from 'react-force-graph-2d'; -import { ReactWidget } from '@jupyterlab/apputils'; -import { fetchNodeConnections } from './handler'; - -interface IOntologyWidgetProps { - fetchData: () => Promise; -} - -export const OntologyWidgetComponent: React.FC = ({ - fetchData -}) => { - const [data, setData] = useState(null); - - useEffect(() => { - const fetchAndSetData = async () => { - try { - const result = await fetchData(); - setData(result); - } catch (error) { - console.error('Failed to fetch data:', error); - } - }; - - fetchAndSetData(); - }, [fetchData]); - - const handleNodeClick = (node: any) => { - console.log('Node clicked'); - const connections = fetchNodeConnections(node.label); - console.log(connections); - }; - - return ( -
-
- {data ? ( - { - 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} - /> - ) : ( -
Loading...
- )} -
-
- ); -}; - -export class OntologyWidget extends ReactWidget { - fetchData: () => Promise; - - constructor(fetchData: () => Promise) { - super(); - this.addClass('tvb-OntologyWidget'); - this.fetchData = fetchData; - } - - render(): React.ReactElement { - return ; - } -} diff --git a/src/components/GaphView.tsx b/src/components/GaphView.tsx new file mode 100644 index 0000000..debb2d2 --- /dev/null +++ b/src/components/GaphView.tsx @@ -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 = ({ + setSelectedNode +}) => { + const [data, setData] = useState<{ nodes: INodeType[]; links: ILinkType[]; }>({ nodes: [], links: [] }); + const [searchLabel, setSearchLabel] = useState(''); + const [treeData, setTreeData] = useState(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(); + + // 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) => { + if (event.key === 'Enter') { + handleSearch(); + } + }; + + return ( +
+ +
+ setSearchLabel(e.target.value)} + onKeyPress={handleKeyPress} + placeholder="Enter node label" + /> + +
+
+ {data ? ( + { + 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} + /> + ) : ( +
Loading...
+ )} +
+
+ ); +}; diff --git a/src/components/InfoBox.tsx b/src/components/InfoBox.tsx index c5a1892..5b08dbe 100644 --- a/src/components/InfoBox.tsx +++ b/src/components/InfoBox.tsx @@ -1,7 +1,62 @@ import React from 'react'; +import { ISelectedNodeType } from './interfaces/InfoBoxInterfaces'; -const InfoBoxComponent: React.FC = () => { - return
Info Box
; +interface IInfoBoxProps { + selectedNode: { + label: string; + type: string; + definition: string; + iri: string; + } | null; + addToWorkspace: (node: ISelectedNodeType) => void; +} + +const InfoBoxComponent: React.FC = ({ + 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 ( +
+ {selectedNode ? ( +
+

Node Information

+

+ Name: {selectedNode.label} +

+

+ Type: {selectedNode.type} +

+

+ Definition: {selectedNode.definition} +

+

+ IRI:{' '} + + {selectedNode.iri} + +

+ +
+ ) : ( +

Select a node to see its details here

+ )} +
+ ); }; export default InfoBoxComponent; diff --git a/src/components/TreeView.tsx b/src/components/TreeView.tsx index 05f6f91..4db0073 100644 --- a/src/components/TreeView.tsx +++ b/src/components/TreeView.tsx @@ -1,7 +1,40 @@ import React from 'react'; +import { ITreeNode } from './interfaces/TreeViewInterfaces'; -const TreeViewComponent: React.FC = () => { - return
Tree View
; +interface ITreeViewProps { + treeData: ITreeNode | null; +} + +const TreeViewComponent: React.FC = ({ treeData }) => { + const renderTree = (node: ITreeNode) => ( +
    + {node.parents.map(parent => ( +
  • + {parent.label} (Parent) + {parent.children.length > 0 && renderTree(parent)} +
  • + ))} +
  • + {node.label} (Current) + {node.children.length > 0 && ( +
      + {node.children.map(child => ( +
    • + {child.label} (Child) + {child.children.length > 0 && renderTree(child)} +
    • + ))} +
    + )} +
  • +
+ ); + return ( +
+

Tree View

+ {treeData ? renderTree(treeData) :

Please select a node first

} +
+ ); }; export default TreeViewComponent; diff --git a/src/components/Workspace.tsx b/src/components/Workspace.tsx index c71fcf9..3b8864e 100644 --- a/src/components/Workspace.tsx +++ b/src/components/Workspace.tsx @@ -1,7 +1,32 @@ import React from 'react'; +import { IWorkspaceProps } from './interfaces/WorkspaceInterfaces'; -const WorkspaceComponent: React.FC = () => { - return
Workspace
; +const WorkspaceComponent: React.FC = ({ workspace }) => { + return ( +
+

Workspace

+
+

Model

+ {workspace.model ?
{workspace.model.label}
:
No model selected
} +
+
+

Connectivity

+ {workspace.connectivity ?
{workspace.connectivity.label}
:
No connectivity selected
} +
+
+

Coupling

+ {workspace.coupling ?
{workspace.coupling.label}
:
No coupling selected
} +
+
+

Noise

+ {workspace.noise ?
{workspace.noise.label}
:
No noise selected
} +
+
+

Integration Method

+ {workspace.integrationMethod ?
{workspace.integrationMethod.label}
:
No integration method selected
} +
+
+ ); }; export default WorkspaceComponent; diff --git a/src/components/interfaces/GraphViewInterfaces.tsx b/src/components/interfaces/GraphViewInterfaces.tsx new file mode 100644 index 0000000..2cd5470 --- /dev/null +++ b/src/components/interfaces/GraphViewInterfaces.tsx @@ -0,0 +1,17 @@ +export interface INodeType { + id: number; + label: string; + type: string; + definition: string; + iri: string; + x?: number; + y?: number; + collapsed?: boolean; + childLinks?: ILinkType[]; +} + +export interface ILinkType { + source: number; + target: number; + type: string; +} diff --git a/src/components/interfaces/InfoBoxInterfaces.tsx b/src/components/interfaces/InfoBoxInterfaces.tsx new file mode 100644 index 0000000..4b9ef8d --- /dev/null +++ b/src/components/interfaces/InfoBoxInterfaces.tsx @@ -0,0 +1,10 @@ +import { ILinkType } from './GraphViewInterfaces'; + +export interface ISelectedNodeType { + label: string; + type: string; + definition: string; + iri: string; + childLinks?: ILinkType[], + collapsed?: boolean; +} diff --git a/src/components/interfaces/TreeViewInterfaces.tsx b/src/components/interfaces/TreeViewInterfaces.tsx new file mode 100644 index 0000000..aee1d90 --- /dev/null +++ b/src/components/interfaces/TreeViewInterfaces.tsx @@ -0,0 +1,7 @@ +export interface ITreeNode { + id: number; + label: string; + type: string; + children: ITreeNode[]; + parents: ITreeNode[]; +} diff --git a/src/components/interfaces/WorkspaceInterfaces.tsx b/src/components/interfaces/WorkspaceInterfaces.tsx new file mode 100644 index 0000000..3b5f773 --- /dev/null +++ b/src/components/interfaces/WorkspaceInterfaces.tsx @@ -0,0 +1,13 @@ +import { ISelectedNodeType } from './InfoBoxInterfaces'; + +export interface IWorkspaceProps { + workspace: IWorkspaceState; +} + +export interface IWorkspaceState { + model: ISelectedNodeType | null; + connectivity: ISelectedNodeType | null; + coupling: ISelectedNodeType | null; + noise: ISelectedNodeType | null; + integrationMethod: ISelectedNodeType | null; +} diff --git a/src/handler.ts b/src/handler.ts index 445d3e2..a29ccd8 100644 --- a/src/handler.ts +++ b/src/handler.ts @@ -1,6 +1,7 @@ import { URLExt } from '@jupyterlab/coreutils'; import { ServerConnection } from '@jupyterlab/services'; +import { ILinkType, INodeType } from './components/interfaces/GraphViewInterfaces'; /** * Call the API extension @@ -45,7 +46,7 @@ export async function requestAPI( return data; } -export async function fetchNodeByLabel(label: string): Promise { +export async function fetchNodeByLabel(label: string): Promise { try { const response = await requestAPI(`node?label=${label}`); return response; @@ -54,14 +55,12 @@ export async function fetchNodeByLabel(label: string): Promise { } } -export async function fetchNodeConnections(label: string): Promise { +export async function fetchNodeConnections(label: string): Promise<{ nodes: INodeType[]; links: ILinkType[] } | null> { try { - const response = await requestAPI(`node-connections?label=${label}`); - console.log('Label from handler.ts: ', label); - console.log('Response from handlers.ts: ', response); - console.log(typeof response); + const response = await requestAPI<{ nodes: INodeType[]; links: ILinkType[] }>(`node-connections?label=${label}`); return response; } catch (error) { console.error(`Error fetching node data: ${error}`); + return null; } } diff --git a/src/index.ts b/src/index.ts index 6daefed..2dea198 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,7 @@ import { JupyterFrontEndPlugin } from '@jupyterlab/application'; -import { fetchNodeByLabel, fetchNodeConnections } from './handler'; +import { fetchNodeByLabel } from './handler'; import { ILauncher } from '@jupyterlab/launcher'; import { LabIcon } from '@jupyterlab/ui-components'; diff --git a/style/layout.css b/style/layout.css index 6a2b1c4..9ab88d2 100644 --- a/style/layout.css +++ b/style/layout.css @@ -1,7 +1,7 @@ .layout { display: grid; grid-template: - 'search-bar tree-view info-box' auto + 'tree-view tree-view info-box' auto 'ontology-graph ontology-graph info-box' 1fr 'ontology-graph ontology-graph workspace' 1fr / 1fr 1fr 1fr; height: 90vh; @@ -48,3 +48,39 @@ overflow: auto; background-color: whitesmoke; } + +.search-bar { + display: flex; + margin-bottom: 10px; +} + +.search-bar input { + flex: 1; + padding: 5px; + font-size: 16px; + border: 1px solid #ccc; + border-radius: 4px; +} + +.search-bar button, +.info-box button{ + margin-left: 10px; + padding: 5px 10px; + font-size: 16px; + background-color: #007bff; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; +} + +.search-bar button:hover, +.info-box button:hover{ + background-color: #0056b3; +} + +/* Disabled button style */ +.info-box button:disabled { + background-color: #ccc; + cursor: not-allowed; +} diff --git a/tvb_ext_ontology/handlers.py b/tvb_ext_ontology/handlers.py index 1ea83b1..9a1b3b9 100644 --- a/tvb_ext_ontology/handlers.py +++ b/tvb_ext_ontology/handlers.py @@ -5,6 +5,8 @@ import tornado from tvbo.api.ontology_api import OntologyAPI +onto_api = OntologyAPI() + class RouteHandler(APIHandler): # The following decorator should be present on all verb methods (head, get, post, @@ -26,8 +28,8 @@ def get(self): self.finish(json.dumps({"error": "Missing 'label' parameter"})) return - onto_api = OntologyAPI() - node_data = onto_api.query_nodes(label) + node_data = onto_api.get_node_by_label(label) + print(f'Links: {node_data["links"]}') if not node_data: self.set_status(404) self.finish(json.dumps({"error": f"No data found for label: {label}"})) @@ -46,8 +48,11 @@ def get(self): self.finish(json.dumps({"error": "Missing 'label' parameter"})) return - onto_api = OntologyAPI() - node_data = onto_api.expand_node_relationships(label) + onto_api.expand_node_relationships(label) + nodes = onto_api.nodes + links = onto_api.edges + node_data = {'nodes': nodes, 'links': links} + print(f'Connections links: {node_data["links"]}') if not node_data: self.set_status(404) self.finish(json.dumps({"error": f"No data found for label: {label}"}))