Skip to content

Commit

Permalink
VT-23. Add functionalities to display direct children and parents of …
Browse files Browse the repository at this point in the history
…selected node in a separate view; export workspace; refactor layout; add tvbo icon in launcher button;
  • Loading branch information
romina1601 committed Sep 2, 2024
1 parent d2f9d6a commit bd85731
Show file tree
Hide file tree
Showing 13 changed files with 386 additions and 149 deletions.
16 changes: 5 additions & 11 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,9 @@ import InfoBoxComponent from './components/InfoBox';
import WorkspaceComponent from './components/Workspace';
import { ISelectedNodeType } from './components/interfaces/InfoBoxInterfaces';
import { IWorkspaceState } from './components/interfaces/WorkspaceInterfaces';
import TreeViewComponent from './components/TreeView';

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

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

const [workspace, setWorkspace] = useState<IWorkspaceState>({
Expand All @@ -26,12 +23,8 @@ const App: React.FC<IAppProps> = () => {
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 'Noise':
return { ...prevWorkspace, noise: node };
case 'Coupling':
return { ...prevWorkspace, coupling: node };
case 'Integrator':
Expand All @@ -46,6 +39,7 @@ const App: React.FC<IAppProps> = () => {
<div className="layout">
<InfoBoxComponent selectedNode={selectedNode} addToWorkspace={addToWorkspace} />
<WorkspaceComponent workspace={workspace} />
<TreeViewComponent selectedNode={selectedNode} />
<GraphViewComponent setSelectedNode={setSelectedNode} />
</div>
);
Expand Down
6 changes: 2 additions & 4 deletions src/AppWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,12 @@ import React from 'react';
import App from './App';

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

render(): React.ReactElement {
return <App fetchData={this.fetchData} />;
return <App />;
}
}
57 changes: 3 additions & 54 deletions src/components/GaphView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import ForceGraph2D from 'react-force-graph-2d';
import { fetchNodeByLabel, fetchNodeChildren } 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;
Expand All @@ -15,7 +13,6 @@ export const GraphViewComponent: React.FC<IGraphViewProps> = ({
}) => {
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) => {
Expand All @@ -32,47 +29,9 @@ export const GraphViewComponent: React.FC<IGraphViewProps> = ({
fetchAndSetData(searchLabel);
}, [searchLabel]);

const buildTree = (currentNode: INodeType): ITreeNode => {
const nodeMap = new Map<string, 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: []
});
});

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

// Get parents and children
data.links.forEach(link => {
const sourceNode = nodeMap.get(link.source);
const targetNode = nodeMap.get(link.target);

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({
id: node.id,
label: node.label,
type: node.type,
definition: node.definition,
Expand All @@ -84,16 +43,9 @@ export const GraphViewComponent: React.FC<IGraphViewProps> = ({
console.log('Node clicked: ', node);
node.collapsed = !node.collapsed;

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

const connections = await fetchNodeChildren(node.label, node.id);
console.log('GraphView connections: ', connections);
node.childNodes = connections.nodes;
node.childLinks = connections.links;
console.log(node.childNodes);
console.log(node.childLinks);

const visibleNodes: INodeType[] = [];
const visibleLinks: ILinkType[] = [];
Expand All @@ -106,7 +58,6 @@ export const GraphViewComponent: React.FC<IGraphViewProps> = ({
}

const processNode = (n: INodeType) => {
console.log('PROCESSING NODE: ', n);
if (!visitedIds.includes(n.id)) {
visitedIds.push(n.id);
visibleNodes.push(n);
Expand All @@ -124,7 +75,6 @@ export const GraphViewComponent: React.FC<IGraphViewProps> = ({
processNode(node);
newNodes = [...data.nodes, ...visibleNodes];
newLinks = [...data.links, ...visibleLinks];
console.log(newNodes);
setData({ nodes: newNodes, links: newLinks });
};

Expand All @@ -144,7 +94,6 @@ export const GraphViewComponent: React.FC<IGraphViewProps> = ({

return (
<div className="ontology-graph">
<TreeViewComponent treeData={treeData} />
<div className="search-bar">
<input
type="text"
Expand All @@ -160,7 +109,7 @@ export const GraphViewComponent: React.FC<IGraphViewProps> = ({
<ForceGraph2D
graphData={data}
onNodeClick={handleNodeClick}
linkCurvature={0.25}
linkCurvature={0.15}
nodeCanvasObject={(node, ctx, globalScale) => {
const label = node.label;
const fontSize = 12 / globalScale;
Expand All @@ -178,7 +127,7 @@ export const GraphViewComponent: React.FC<IGraphViewProps> = ({
linkDirectionalArrowRelPos={1}
/>
) : (
<div>Loading...</div>
<div>Search for a term</div>
)}
</div>
</div>
Expand Down
58 changes: 30 additions & 28 deletions src/components/InfoBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ISelectedNodeType } from './interfaces/InfoBoxInterfaces';

interface IInfoBoxProps {
selectedNode: {
id: string;
label: string;
type: string;
definition: string;
Expand All @@ -16,12 +17,8 @@ const InfoBoxComponent: React.FC<IInfoBoxProps> = ({
addToWorkspace
}) => {
// Valid types for adding objects to workspace
const validTypes = [
'Neural Mass Model',
'TheVirtualBrain', // TODO: change to actual type for connectivity, noise
'Coupling',
'Integrator'
];
// TODO: add valid type for connectivity
const validTypes = ['Neural Mass Model', 'Noise', 'Coupling', 'Integrator'];

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

Expand All @@ -30,28 +27,33 @@ const InfoBoxComponent: React.FC<IInfoBoxProps> = ({
<h3>Concept Details</h3>
{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 className="node-info-container">
<div className="node-info">
<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>
</div>
<button
className="add-button"
onClick={() => addToWorkspace(selectedNode)}
disabled={!isAddable}
>
Add to Workspace
</button>
</div>
</div>
) : (
<p>Select a node to see its details here</p>
Expand Down
132 changes: 102 additions & 30 deletions src/components/TreeView.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,110 @@
import React from 'react';
import { ITreeNode } from './interfaces/TreeViewInterfaces';
import React, { useEffect, useState } from 'react';
import { ILinkType, INodeType } from './interfaces/GraphViewInterfaces';
import { fetchNodeChildren, fetchNodeParents } from '../handler';

interface ITreeViewProps {
treeData: ITreeNode | null;
selectedNode: INodeType | null;
}

const TreeViewComponent: React.FC<ITreeViewProps> = ({ treeData }) => {
const renderTree = (node: ITreeNode) => (
<ul key={node.id}>
{node.parents.map(parent => (
<li key={parent.id}>
{parent.label} (Parent)
{parent.children.length > 0 && renderTree(parent)}
</li>
))}
<li>
<strong>{node.label} (Current)</strong>
{node.children.length > 0 && (
<ul>
{node.children.map(child => (
<li key={child.id}>
{child.label} (Child)
{child.children.length > 0 && renderTree(child)}
</li>
))}
</ul>
)}
</li>
</ul>
);
interface INodeRelation {
node: INodeType;
relation: string;
}

const TreeViewComponent: React.FC<ITreeViewProps> = ({ selectedNode }) => {
const [parents, setParents] = useState<INodeRelation[]>([]);
const [children, setChildren] = useState<INodeRelation[]>([]);

useEffect(() => {
if (!selectedNode) {
setParents([]);
setChildren([]);
return;
}

const fetchAndSetParents = async () => {
try {
const { nodes, links } = await fetchNodeParents(selectedNode.label, selectedNode.id); // Fetch raw data
const parentData = processNodeRelations(nodes, links, selectedNode.id, 'parents');
setParents(parentData);
} catch (error) {
console.error('Failed to fetch parents:', error);
}
};

const fetchAndSetChildren = async () => {
try {
const { nodes, links } = await fetchNodeChildren(selectedNode.label, selectedNode.id); // Fetch raw data
const childData = processNodeRelations(nodes, links, selectedNode.id, 'children');
setChildren(childData);
} catch (error) {
console.error('Failed to fetch children:', error);
}
};

fetchAndSetParents();
fetchAndSetChildren();
}, [selectedNode]);

const processNodeRelations = (
nodes: INodeType[],
links: ILinkType[],
currentNodeId: string,
type: 'parents' | 'children'
): INodeRelation[] => {
return links
.filter((link: ILinkType) =>
type === 'parents' ? link.target === currentNodeId : link.source === currentNodeId
)
.map((link: ILinkType) => {
const relatedNode = nodes.find((node: INodeType) =>
type === 'parents' ? node.id === link.source : node.id === link.target
);
return relatedNode ? { node: relatedNode, relation: link.type } : null;
})
.filter(Boolean) as INodeRelation[]; // Remove any null values
};

return (
<div>
<h3>Tree View</h3>
{treeData ? renderTree(treeData) : <p>Please select a node first</p>}
<div className="tree-view">
{selectedNode ? (
<>
<h2 style={{ textAlign: 'center' }}>{selectedNode.label}</h2>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<div>
<h3>Parents</h3>
{parents.length > 0 ? (
<ul>
{parents.map(({ node, relation }) => (
<li key={node.id}>
{node.label} ({relation})
</li>
))}
</ul>
) : (
<p>No parents found.</p>
)}
</div>

<div>
<h3>Children</h3>
{children.length > 0 ? (
<ul>
{children.map(({ node, relation }) => (
<li key={node.id}>
{node.label} ({relation})
</li>
))}
</ul>
) : (
<p>No children found.</p>
)}
</div>
</div>
</>
) : (
<p>Please select a node first</p>
)}
</div>
);
};
Expand Down
Loading

0 comments on commit bd85731

Please sign in to comment.