diff --git a/applications/visualizer/frontend/src/components/viewers/TwoD/ContextMenu.tsx b/applications/visualizer/frontend/src/components/viewers/TwoD/ContextMenu.tsx index 1469c49d..4ef648ae 100644 --- a/applications/visualizer/frontend/src/components/viewers/TwoD/ContextMenu.tsx +++ b/applications/visualizer/frontend/src/components/viewers/TwoD/ContextMenu.tsx @@ -1,94 +1,165 @@ -import React, { useMemo } from "react"; -import { Menu, MenuItem } from "@mui/material"; +import React, {useMemo} from "react"; +import {Menu, MenuItem} from "@mui/material"; import {NeuronGroup, Workspace} from "../../../models"; +import {isClass} from "../../../helpers/twoD/twoDHelpers.ts"; interface ContextMenuProps { - open: boolean; - onClose: () => void; - workspace: Workspace; - position: { mouseX: number; mouseY: number } | null; + open: boolean; + onClose: () => void; + workspace: Workspace; + position: { mouseX: number; mouseY: number } | null; + setSplitJoinState: React.Dispatch; join: Set }>>; } -const ContextMenu: React.FC = ({ open, onClose, workspace, position }) => { - const handleClearSelections = () => { - workspace.clearSelectedNeurons(); - onClose(); - }; - - const handleGroup = () => { - const newGroupId = `group_${Date.now()}`; - const newGroupNeurons = new Set(); - - for (const neuronId of workspace.selectedNeurons) { - const group = workspace.neuronGroups[neuronId]; - if (group) { - for (const groupedNeuronId of group.neurons) { - newGroupNeurons.add(groupedNeuronId); - } - } else { - newGroupNeurons.add(neuronId); - } - } - - const newGroup: NeuronGroup = { - id: newGroupId, - name: newGroupId, - color: "#9FEE9A", - neurons: newGroupNeurons, +const ContextMenu: React.FC = ({open, onClose, workspace, position, setSplitJoinState}) => { + const handleClearSelections = () => { + workspace.clearSelectedNeurons(); + onClose(); }; - workspace.batchUpdate(draft => { - draft.neuronGroups[newGroupId] = newGroup; - draft.selectedNeurons.clear(); - draft.selectedNeurons.add(newGroupId); - }); - onClose(); - }; - - const handleUngroup = () => { - workspace.batchUpdate(draft => { - for (const elementId of draft.selectedNeurons) { - if (draft.neuronGroups[elementId]) { - const group = draft.neuronGroups[elementId]; - for (const groupedNeuronId of group.neurons) { - draft.selectedNeurons.add(groupedNeuronId); - } - delete draft.neuronGroups[elementId]; + const handleGroup = () => { + const newGroupId = `group_${Date.now()}`; + const newGroupNeurons = new Set(); + + for (const neuronId of workspace.selectedNeurons) { + const group = workspace.neuronGroups[neuronId]; + if (group) { + for (const groupedNeuronId of group.neurons) { + newGroupNeurons.add(groupedNeuronId); + } + } else { + newGroupNeurons.add(neuronId); + } } - } - }); - onClose(); - }; - - const groupEnabled = useMemo(() => { - return Array.from(workspace.selectedNeurons).some((neuronId) => !workspace.neuronGroups[neuronId]); - }, [workspace.selectedNeurons, workspace.neuronGroups]); - - const ungroupEnabled = useMemo(() => { - return Array.from(workspace.selectedNeurons).some((neuronId) => workspace.neuronGroups[neuronId]); - }, [workspace.selectedNeurons, workspace.neuronGroups]); - - const handleContextMenu = (event: React.MouseEvent) => { - event.preventDefault(); // Prevent default context menu - }; - - return ( - - Clear Selections - - Group - - - Ungroup - - - ); + + const newGroup: NeuronGroup = { + id: newGroupId, + name: newGroupId, + color: "#9FEE9A", + neurons: newGroupNeurons, + }; + + workspace.batchUpdate(draft => { + draft.neuronGroups[newGroupId] = newGroup; + draft.selectedNeurons.clear(); + draft.selectedNeurons.add(newGroupId); + }); + onClose(); + }; + + const handleUngroup = () => { + workspace.batchUpdate(draft => { + for (const elementId of draft.selectedNeurons) { + if (draft.neuronGroups[elementId]) { + const group = draft.neuronGroups[elementId]; + for (const groupedNeuronId of group.neurons) { + draft.selectedNeurons.add(groupedNeuronId); + } + delete draft.neuronGroups[elementId]; + } + } + }); + onClose(); + }; + + const handleSplit = () => { + setSplitJoinState((prevState) => { + const newSplit = new Set(prevState.split); + const newJoin = new Set(prevState.join); + + workspace.selectedNeurons.forEach(neuronId => { + if (isClass(neuronId, workspace)) { + newSplit.add(neuronId); + // Remove the corresponding class from the toJoin set + newJoin.forEach(joinNeuronId => { + if (workspace.availableNeurons[joinNeuronId].nclass === neuronId) { + newJoin.delete(joinNeuronId); + } + }); + } + }); + + return {split: newSplit, join: newJoin}; + }); + onClose(); + }; + + const handleJoin = () => { + setSplitJoinState((prevState) => { + const newJoin = new Set(prevState.join); + const newSplit = new Set(prevState.split); + + workspace.selectedNeurons.forEach(neuronId => { + const neuronClass = workspace.availableNeurons[neuronId].nclass; + Object.values(workspace.availableNeurons).forEach(neuron => { + if (neuron.nclass === neuronClass && neuron.name !== neuron.nclass) { + newJoin.add(neuron.name); + } + }); + + // Remove the corresponding cells from the toSplit set + newSplit.forEach(splitNeuronId => { + if (workspace.availableNeurons[splitNeuronId].nclass === neuronClass) { + newSplit.delete(splitNeuronId); + } + }); + }); + + return {split: newSplit, join: newJoin}; + }); + onClose(); + }; + + const groupEnabled = useMemo(() => { + return Array.from(workspace.selectedNeurons).some((neuronId) => !workspace.neuronGroups[neuronId]); + }, [workspace.selectedNeurons, workspace.neuronGroups]); + + const ungroupEnabled = useMemo(() => { + return Array.from(workspace.selectedNeurons).some((neuronId) => workspace.neuronGroups[neuronId]); + }, [workspace.selectedNeurons, workspace.neuronGroups]); + + const splitEnabled = useMemo(() => { + return Array.from(workspace.selectedNeurons).some((neuronId) => { + const neuron = workspace.availableNeurons[neuronId]; + return neuron && neuron.name === neuron.nclass; + }); + }, [workspace.selectedNeurons, workspace.availableNeurons]); + + const joinEnabled = useMemo(() => { + return Array.from(workspace.selectedNeurons).some((neuronId) => { + const neuron = workspace.availableNeurons[neuronId]; + return neuron && neuron.name !== neuron.nclass; + }); + }, [workspace.selectedNeurons, workspace.availableNeurons]); + + + const handleContextMenu = (event: React.MouseEvent) => { + event.preventDefault(); // Prevent default context menu + }; + + return ( + + Clear Selections + + Group + + + Ungroup + + + Join Left-Right + + + Split Left-Right + + + ); }; export default ContextMenu; diff --git a/applications/visualizer/frontend/src/components/viewers/TwoD/TwoDViewer.tsx b/applications/visualizer/frontend/src/components/viewers/TwoD/TwoDViewer.tsx index a7624bb7..28f9f320 100644 --- a/applications/visualizer/frontend/src/components/viewers/TwoD/TwoDViewer.tsx +++ b/applications/visualizer/frontend/src/components/viewers/TwoD/TwoDViewer.tsx @@ -1,238 +1,290 @@ -import React, { useState, useEffect, useRef } from "react"; -import cytoscape, { type Core } from "cytoscape"; +import React, {useState, useEffect, useRef} from "react"; +import cytoscape, {type Core} from "cytoscape"; import fcose from "cytoscape-fcose"; import dagre from "cytoscape-dagre"; -import { useSelectedWorkspace } from "../../../hooks/useSelectedWorkspace"; -import { type Connection, ConnectivityService } from "../../../rest"; -import { GRAPH_STYLES } from "../../../theme/twoDStyles"; -import { applyLayout, updateHighlighted } from "../../../helpers/twoD/twoDHelpers"; -import { CHEMICAL_THRESHOLD, ELECTRICAL_THRESHOLD, GRAPH_LAYOUTS, INCLUDE_ANNOTATIONS, INCLUDE_NEIGHBORING_CELLS } from "../../../settings/twoDSettings"; +import {useSelectedWorkspace} from "../../../hooks/useSelectedWorkspace"; +import {type Connection, ConnectivityService} from "../../../rest"; +import {GRAPH_STYLES} from "../../../theme/twoDStyles"; +import {applyLayout, updateHighlighted} from "../../../helpers/twoD/twoDHelpers"; +import { + CHEMICAL_THRESHOLD, + ELECTRICAL_THRESHOLD, + GRAPH_LAYOUTS, + INCLUDE_ANNOTATIONS, + INCLUDE_NEIGHBORING_CELLS +} from "../../../settings/twoDSettings"; import TwoDMenu from "./TwoDMenu"; import TwoDLegend from "./TwoDLegend"; -import { Box } from "@mui/material"; -import { ColoringOptions, getColor } from "../../../helpers/twoD/coloringHelper"; +import {Box} from "@mui/material"; +import {ColoringOptions, getColor} from "../../../helpers/twoD/coloringHelper"; import ContextMenu from "./ContextMenu"; -import { computeGraphDifferences } from "../../../helpers/twoD/graphRendering.ts"; +import {computeGraphDifferences} from "../../../helpers/twoD/graphRendering.ts"; cytoscape.use(fcose); cytoscape.use(dagre); const TwoDViewer = () => { - const workspace = useSelectedWorkspace(); - const cyContainer = useRef(null); - const cyRef = useRef(null); - const [connections, setConnections] = useState([]); - const [layout, setLayout] = useState(GRAPH_LAYOUTS.Concentric); - const [coloringOption, setColoringOption] = useState(ColoringOptions.CELL_TYPE); - const [thresholdChemical, setThresholdChemical] = useState(CHEMICAL_THRESHOLD); - const [thresholdElectrical, setThresholdElectrical] = useState(ELECTRICAL_THRESHOLD); - const [includeNeighboringCells, setIncludeNeighboringCells] = useState(INCLUDE_NEIGHBORING_CELLS); - const [includeNeighboringCellsAsIndividualCells, setIncludeNeighboringCellsAsIndividualCells] = useState(false); - const [toSplit, setToSplit] = useState>(new Set()); - const [toJoin, setToJoin] = useState>(new Set()); - const [includeAnnotations, setIncludeAnnotations] = useState(INCLUDE_ANNOTATIONS); - const [mousePosition, setMousePosition] = useState<{ mouseX: number; mouseY: number } | null>(null); - - const handleContextMenuClose = () => { - setMousePosition(null); - }; - - // Initialize and update Cytoscape - useEffect(() => { - if (!cyContainer.current) return; - - const cy = cytoscape({ - container: cyContainer.current, - style: GRAPH_STYLES, - layout: { - name: layout, - }, + const workspace = useSelectedWorkspace(); + const cyContainer = useRef(null); + const cyRef = useRef(null); + const [connections, setConnections] = useState([]); + const [layout, setLayout] = useState(GRAPH_LAYOUTS.Concentric); + const [coloringOption, setColoringOption] = useState(ColoringOptions.CELL_TYPE); + const [thresholdChemical, setThresholdChemical] = useState(CHEMICAL_THRESHOLD); + const [thresholdElectrical, setThresholdElectrical] = useState(ELECTRICAL_THRESHOLD); + const [includeNeighboringCells, setIncludeNeighboringCells] = useState(INCLUDE_NEIGHBORING_CELLS); + const [includeNeighboringCellsAsIndividualCells, setIncludeNeighboringCellsAsIndividualCells] = useState(false); + const [splitJoinState, setSplitJoinState] = useState<{ split: Set; join: Set }>({ + split: new Set(), + join: new Set() }); - cyRef.current = cy; + const [includeAnnotations, setIncludeAnnotations] = useState(INCLUDE_ANNOTATIONS); + const [mousePosition, setMousePosition] = useState<{ mouseX: number; mouseY: number } | null>(null); - const resizeObserver = new ResizeObserver((entries) => { - for (const entry of entries) { - if (entry.target === cyContainer.current) { - updateLayout(); + const handleContextMenuClose = () => { + setMousePosition(null); + }; + + // Initialize and update Cytoscape + useEffect(() => { + if (!cyContainer.current) return; + + const cy = cytoscape({ + container: cyContainer.current, + style: GRAPH_STYLES, + layout: { + name: layout, + }, + }); + cyRef.current = cy; + + const resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + if (entry.target === cyContainer.current) { + updateLayout(); + } + } + }); + resizeObserver.observe(cyContainer.current); + + cyContainer.current.addEventListener("contextmenu", (event) => { + event.preventDefault(); + }); + + return () => { + resizeObserver.disconnect(); + cy.destroy(); + }; + }, []); + + // Fetch and process connections data + useEffect(() => { + if (!workspace) return; + + // Convert activeNeurons and activeDatasets to comma-separated strings + const cells = Array.from(workspace.activeNeurons || []).join(","); + const datasetIds = Object.values(workspace.activeDatasets) + .map((dataset) => dataset.id) + .join(","); + const datasetType = Object.values(workspace.activeDatasets) + .map((dataset) => dataset.type) + .join(","); + + ConnectivityService.getConnections({ + cells, + datasetIds, + datasetType, + thresholdChemical: thresholdChemical, + thresholdElectrical: thresholdElectrical, + includeNeighboringCells: includeNeighboringCells, + includeAnnotations: includeAnnotations, + }) + .then((connections) => { + setConnections(connections); + }) + .catch((error) => { + console.error("Failed to fetch connections:", error); + }); + }, [workspace, includeNeighboringCells, includeNeighboringCellsAsIndividualCells, includeAnnotations, thresholdElectrical, thresholdChemical]); + + // Update graph when connections change + useEffect(() => { + if (cyRef.current) { + updateGraphElements(cyRef.current, connections); } - } - }); - resizeObserver.observe(cyContainer.current); + }, [connections]); - cyContainer.current.addEventListener("contextmenu", (event) => { - event.preventDefault(); - }); + useEffect(() => { + if (cyRef.current) { + updateNodeColors(); + } + }, [coloringOption]); - return () => { - resizeObserver.disconnect(); - cy.destroy(); - }; - }, []); - - // Fetch and process connections data - useEffect(() => { - if (!workspace) return; - - // Convert activeNeurons and activeDatasets to comma-separated strings - const cells = Array.from(workspace.activeNeurons || []).join(","); - const datasetIds = Object.values(workspace.activeDatasets) - .map((dataset) => dataset.id) - .join(","); - const datasetType = Object.values(workspace.activeDatasets) - .map((dataset) => dataset.type) - .join(","); - - ConnectivityService.getConnections({ - cells, - datasetIds, - datasetType, - thresholdChemical: thresholdChemical, - thresholdElectrical: thresholdElectrical, - includeNeighboringCells: includeNeighboringCells, - includeAnnotations: includeAnnotations, - }) - .then((connections) => { - setConnections(connections); - }) - .catch((error) => { - console.error("Failed to fetch connections:", error); - }); - }, [workspace, includeNeighboringCells, includeNeighboringCellsAsIndividualCells, includeAnnotations, thresholdElectrical, thresholdChemical]); - - // Update graph when connections change - useEffect(() => { - if (cyRef.current) { - updateGraphElements(cyRef.current, connections); - } - }, [connections]); - - useEffect(() => { - if (cyRef.current) { - updateNodeColors(); - } - }, [coloringOption]); - - // Update layout when layout setting changes - useEffect(() => { - updateLayout(); - }, [layout]); - - // Add event listener for node clicks to toggle neuron selection and right-click context menu - useEffect(() => { - if (!cyRef.current) return; - - const cy = cyRef.current; - - const handleNodeClick = (event) => { - const neuronId = event.target.id(); - const isSelected = workspace.selectedNeurons.has(neuronId); - workspace.toggleSelectedNeuron(neuronId); - - if (isSelected) { - event.target.removeClass("selected"); - } else { - event.target.addClass("selected"); - } - }; + // Update layout when layout setting changes + useEffect(() => { + updateLayout(); + }, [layout]); - const handleBackgroundClick = (event) => { - if (event.target === cy) { - workspace.clearSelectedNeurons(); - cy.nodes(".selected").removeClass("selected"); - } - }; + // Add event listener for node clicks to toggle neuron selection and right-click context menu + useEffect(() => { + if (!cyRef.current) return; + + const cy = cyRef.current; + + const handleNodeClick = (event) => { + const neuronId = event.target.id(); + const isSelected = workspace.selectedNeurons.has(neuronId); + workspace.toggleSelectedNeuron(neuronId); - const handleContextMenu = (event: MouseEvent) => { - event.preventDefault(); + if (isSelected) { + event.target.removeClass("selected"); + } else { + event.target.addClass("selected"); + } + }; - if (workspace.selectedNeurons.size > 0) { - setMousePosition({ - mouseX: event.originalEvent.clientX, - mouseY: event.originalEvent.clientY, + const handleBackgroundClick = (event) => { + if (event.target === cy) { + workspace.clearSelectedNeurons(); + cy.nodes(".selected").removeClass("selected"); + } + }; + + const handleContextMenu = (event: MouseEvent) => { + event.preventDefault(); + + if (workspace.selectedNeurons.size > 0) { + setMousePosition({ + mouseX: event.originalEvent.clientX, + mouseY: event.originalEvent.clientY, + }); + } else { + setMousePosition(null); + } + }; + + cy.on("tap", "node", handleNodeClick); + cy.on("tap", handleBackgroundClick); + cy.on("cxttap", handleContextMenu); + + return () => { + cy.off("tap", "node", handleNodeClick); + cy.off("tap", handleBackgroundClick); + cy.off("cxttap", handleContextMenu); + }; + }, [workspace, connections]); + + + // Update active neurons when split or join state changes + useEffect(() => { + const activeNeurons = new Set(workspace.activeNeurons); + + splitJoinState.split.forEach(neuronId => { + if (workspace.activeNeurons.has(neuronId)) { + activeNeurons.delete(neuronId); + Object.values(workspace.availableNeurons).forEach(neuron => { + if (neuron.nclass === neuronId && neuron.name !== neuron.nclass) { + activeNeurons.add(neuron.name); + } + }); + } + }); + + splitJoinState.join.forEach(neuronId => { + const neuronClass = workspace.availableNeurons[neuronId].nclass; + if (workspace.activeNeurons.has(neuronId)) { + activeNeurons.delete(neuronId); + Object.values(workspace.availableNeurons).forEach(neuron => { + if (neuron.nclass === neuronClass) { + activeNeurons.delete(neuron.name); + } + }); + activeNeurons.add(neuronClass); + } }); - } else { - setMousePosition(null); - } - }; - cy.on("tap", "node", handleNodeClick); - cy.on("tap", handleBackgroundClick); - cy.on("cxttap", handleContextMenu); + workspace.setActiveNeurons(activeNeurons); + }, [splitJoinState, workspace.id]); - return () => { - cy.off("tap", "node", handleNodeClick); - cy.off("tap", handleBackgroundClick); - cy.off("cxttap", handleContextMenu); + const updateGraphElements = (cy: Core, connections: any[]) => { + const { + nodesToAdd, + nodesToRemove, + edgesToAdd, + edgesToRemove + } = computeGraphDifferences(cy, connections, workspace, splitJoinState, includeNeighboringCellsAsIndividualCells); + + cy.batch(() => { + cy.remove(nodesToRemove); + cy.remove(edgesToRemove); + cy.add(nodesToAdd); + cy.add(edgesToAdd); + }); + + updateLayout(); + updateNodeColors(); + updateHighlighted(cy, Array.from(workspace.activeNeurons), Array.from(workspace.selectedNeurons), []); }; - }, [workspace, connections]); - const updateGraphElements = (cy: Core, connections: any[]) => { - const { nodesToAdd, nodesToRemove, edgesToAdd, edgesToRemove } = computeGraphDifferences(cy, connections, workspace, toSplit, toJoin, includeNeighboringCellsAsIndividualCells); + const updateLayout = () => { + if (cyRef.current) { + applyLayout(cyRef, layout); + } + }; - cy.batch(() => { - cy.remove(nodesToRemove); - cy.remove(edgesToRemove); - cy.add(nodesToAdd); - cy.add(edgesToAdd); - }); + const updateNodeColors = () => { + if (!cyRef.current) { + return; + } + cyRef.current.nodes().forEach((node) => { + const neuronId = node.id(); + const neuron = workspace.availableNeurons[neuronId]; + if (neuron == null) { + console.error(`neuron ${neuronId} not found in the active datasets`); + return; + } + const colors = getColor(neuron, coloringOption); + colors.forEach((color, index) => { + node.style(`pie-${index + 1}-background-color`, color); + node.style(`pie-${index + 1}-background-size`, 100 / colors.length); // Equal size for each slice + }); + node.style("pie-background-opacity", 1); + }); + }; - updateLayout(); - updateNodeColors(); - updateHighlighted(cy, Array.from(workspace.activeNeurons), Array.from(workspace.selectedNeurons), []); - }; - - const updateLayout = () => { - if (cyRef.current) { - applyLayout(cyRef, layout); - } - }; - - const updateNodeColors = () => { - if (!cyRef.current) { - return; - } - cyRef.current.nodes().forEach((node) => { - const neuronId = node.id(); - const neuron = workspace.availableNeurons[neuronId]; - if (neuron == null) { - console.error(`neuron ${neuronId} not found in the active datasets`); - return; - } - const colors = getColor(neuron, coloringOption); - colors.forEach((color, index) => { - node.style(`pie-${index + 1}-background-color`, color); - node.style(`pie-${index + 1}-background-size`, 100 / colors.length); // Equal size for each slice - }); - node.style("pie-background-opacity", 1); - }); - }; - - return ( - - - - console.log(`${type} clicked: ${name}`)} /> - -
- - - ); + return ( + + + + console.log(`${type} clicked: ${name}`)}/> + +
+ + + ); }; export default TwoDViewer; diff --git a/applications/visualizer/frontend/src/helpers/twoD/graphRendering.ts b/applications/visualizer/frontend/src/helpers/twoD/graphRendering.ts index b4c9894e..7715f776 100644 --- a/applications/visualizer/frontend/src/helpers/twoD/graphRendering.ts +++ b/applications/visualizer/frontend/src/helpers/twoD/graphRendering.ts @@ -1,212 +1,201 @@ -import type { Core, ElementDefinition, CollectionReturnValue } from 'cytoscape'; -import { createEdge, createNode } from './twoDHelpers'; +import type {Core, ElementDefinition, CollectionReturnValue} from 'cytoscape'; +import {CONNECTION_SEPARATOR, createEdge, createNode, getEdgeName, isCell, isClass} from './twoDHelpers'; import {NeuronGroup, Workspace} from "../../models"; import {Connection} from "../../rest"; -const CONNECTION_SEPARATOR = '-' -export const computeGraphDifferences = (cy: Core, connections: Connection[], workspace: Workspace, toSplit: Set, toJoin: Set, includeNeighboringCellsAsIndividualCells: boolean) => { - // Current nodes and edges in the Cytoscape instance - const currentNodes = new Set(cy.nodes().map((node) => node.id())); - const currentEdges = new Set(cy.edges().map((edge) => edge.id())); - - // Expected nodes and edges - const expectedNodes = new Set(); - const expectedEdges = new Set(); +export const computeGraphDifferences = ( + cy: Core, + connections: Connection[], + workspace: Workspace, + splitJoinState: { split: Set; join: Set }, + includeNeighboringCellsAsIndividualCells: boolean +) => { + // Current nodes and edges in the Cytoscape instance + const currentNodes = new Set(cy.nodes().map((node) => node.id())); + const currentEdges = new Set(cy.edges().map((edge) => edge.id())); + + // Expected nodes and edges + const expectedNodes = new Set(); + const expectedEdges = new Set(); + + const nodesToAdd: ElementDefinition[] = []; + const nodesToRemove: CollectionReturnValue = cy.collection(); + const edgesToAdd: ElementDefinition[] = []; + const edgesToRemove: CollectionReturnValue = cy.collection(); + + // Compute expected nodes based on workspace.activeNeurons and connections + const filteredActiveNeurons = Array.from(workspace.activeNeurons).filter((neuronId: string) => { + const neuron = workspace.availableNeurons[neuronId]; + if (!neuron) { + return false; + } + const nclass = neuron.nclass; + if (neuronId === nclass) { + return true; + } + return !(workspace.activeNeurons.has(neuronId) && workspace.activeNeurons.has(nclass)); + }); + + // Add active neurons to expected nodes + for (const neuronId of filteredActiveNeurons) { + expectedNodes.add(neuronId); + } - const nodesToAdd: ElementDefinition[] = []; - const nodesToRemove: CollectionReturnValue = cy.collection(); - const edgesToAdd: ElementDefinition[] = []; - const edgesToRemove: CollectionReturnValue = cy.collection(); + // Add nodes from connections to expected nodes + for (const conn of connections) { + expectedNodes.add(conn.pre); + expectedNodes.add(conn.post); - // Compute expected nodes based on workspace.activeNeurons and connections - const filteredActiveNeurons = Array.from(workspace.activeNeurons).filter((neuronId: string) => { - const neuron = workspace.availableNeurons[neuronId]; - if (!neuron) { - return false; - } - const nclass = neuron.nclass; - if (neuronId === nclass) { - return true; + const edgeId = getEdgeName(conn.pre, conn.post, conn.type); + expectedEdges.add(edgeId); } - return !(workspace.activeNeurons.has(neuronId) && workspace.activeNeurons.has(nclass)); - }); - - // Add active neurons to expected nodes - for (const neuronId of filteredActiveNeurons) { - expectedNodes.add(neuronId); - } - - // Add nodes from connections to expected nodes - for (const conn of connections) { - expectedNodes.add(conn.pre); - expectedNodes.add(conn.post); - - const edgeId = getEdgeName(conn.pre, conn.post, conn.type); - expectedEdges.add(edgeId); - } - - // Apply split and join rules to expected nodes and edges - applySplitJoinRulesToNodes(expectedNodes, toSplit, toJoin, includeNeighboringCellsAsIndividualCells, workspace); - applySplitJoinRulesToEdges(expectedEdges, toSplit, toJoin, includeNeighboringCellsAsIndividualCells, workspace, expectedNodes); - - // Replace individual neurons and edges with groups if necessary - replaceNodesWithGroups(expectedNodes, workspace.neuronGroups); - replaceEdgesWithGroups(expectedEdges, workspace.neuronGroups); - - // Determine nodes to add and remove - for (const nodeId of expectedNodes) { - if (!currentNodes.has(nodeId)) { - nodesToAdd.push(createNode(nodeId, workspace.selectedNeurons.has(nodeId))); + + // Apply split and join rules to expected nodes and edges + applySplitJoinRulesToNodes(expectedNodes, splitJoinState.split, splitJoinState.join, includeNeighboringCellsAsIndividualCells, workspace); + applySplitJoinRulesToEdges(expectedEdges, splitJoinState.split, splitJoinState.join, includeNeighboringCellsAsIndividualCells, workspace, expectedNodes); + + // Replace individual neurons and edges with groups if necessary + replaceNodesWithGroups(expectedNodes, workspace.neuronGroups); + replaceEdgesWithGroups(expectedEdges, workspace.neuronGroups); + + // Determine nodes to add and remove + for (const nodeId of expectedNodes) { + if (!currentNodes.has(nodeId)) { + nodesToAdd.push(createNode(nodeId, workspace.selectedNeurons.has(nodeId))); + } } - } - for (const nodeId of currentNodes) { - if (!expectedNodes.has(nodeId)) { - nodesToRemove.merge(cy.getElementById(nodeId)); + for (const nodeId of currentNodes) { + if (!expectedNodes.has(nodeId)) { + nodesToRemove.merge(cy.getElementById(nodeId)); + } } - } - // Determine edges to add and remove - for (const edgeId of expectedEdges) { - if (!currentEdges.has(edgeId)) { - const [pre, post, type] = edgeId.split(CONNECTION_SEPARATOR); - edgesToAdd.push(createEdge({ pre, post, type })); + // Determine edges to add and remove + for (const edgeId of expectedEdges) { + if (!currentEdges.has(edgeId)) { + const [pre, post, type] = edgeId.split(CONNECTION_SEPARATOR); + edgesToAdd.push(createEdge({pre, post, type})); + } } - } - for (const edgeId of currentEdges) { - if (!expectedEdges.has(edgeId)) { - edgesToRemove.merge(cy.getElementById(edgeId)); + for (const edgeId of currentEdges) { + if (!expectedEdges.has(edgeId)) { + edgesToRemove.merge(cy.getElementById(edgeId)); + } } - } - // Return the differences to be applied to the Cytoscape instance - return { nodesToAdd, nodesToRemove, edgesToAdd, edgesToRemove }; + // Return the differences to be applied to the Cytoscape instance + return {nodesToAdd, nodesToRemove, edgesToAdd, edgesToRemove}; }; - // Replace individual neurons with group nodes const replaceNodesWithGroups = (expectedNodes: Set, neuronGroups: Record) => { - const nodesToAdd = new Set(); - const nodesToRemove = new Set(); - - expectedNodes.forEach(nodeId => { - for (const groupId in neuronGroups) { - const group = neuronGroups[groupId]; - if (group.neurons.has(nodeId)) { - nodesToAdd.add(groupId); - nodesToRemove.add(nodeId); - } - } - }); - - nodesToRemove.forEach(nodeId => expectedNodes.delete(nodeId)); - nodesToAdd.forEach(nodeId => expectedNodes.add(nodeId)); + const nodesToAdd = new Set(); + const nodesToRemove = new Set(); + + expectedNodes.forEach(nodeId => { + for (const groupId in neuronGroups) { + const group = neuronGroups[groupId]; + if (group.neurons.has(nodeId)) { + nodesToAdd.add(groupId); + nodesToRemove.add(nodeId); + } + } + }); + + nodesToRemove.forEach(nodeId => expectedNodes.delete(nodeId)); + nodesToAdd.forEach(nodeId => expectedNodes.add(nodeId)); }; // Replace edges involving individual neurons with edges involving group nodes const replaceEdgesWithGroups = (expectedEdges: Set, neuronGroups: Record) => { - const edgesToAdd = new Set(); - const edgesToRemove = new Set(); - - expectedEdges.forEach(edgeId => { - const [pre, post, type] = edgeId.split(CONNECTION_SEPARATOR); - - let newPre = pre; - let newPost = post; - - for (const groupId in neuronGroups) { - const group = neuronGroups[groupId]; - if (group.neurons.has(pre)) { - newPre = groupId; - } - if (group.neurons.has(post)) { - newPost = groupId; - } - } - - const newEdgeId = getEdgeName(newPre, newPost, type); - if (newEdgeId !== edgeId) { - edgesToAdd.add(newEdgeId); - edgesToRemove.add(edgeId); - } - }); - - edgesToRemove.forEach(edgeId => expectedEdges.delete(edgeId)); - edgesToAdd.forEach(edgeId => expectedEdges.add(edgeId)); + const edgesToAdd = new Set(); + const edgesToRemove = new Set(); + + expectedEdges.forEach(edgeId => { + const [pre, post, type] = edgeId.split(CONNECTION_SEPARATOR); + + let newPre = pre; + let newPost = post; + + for (const groupId in neuronGroups) { + const group = neuronGroups[groupId]; + if (group.neurons.has(pre)) { + newPre = groupId; + } + if (group.neurons.has(post)) { + newPost = groupId; + } + } + + const newEdgeId = getEdgeName(newPre, newPost, type); + if (newEdgeId !== edgeId) { + edgesToAdd.add(newEdgeId); + edgesToRemove.add(edgeId); + } + }); + + edgesToRemove.forEach(edgeId => expectedEdges.delete(edgeId)); + edgesToAdd.forEach(edgeId => expectedEdges.add(edgeId)); }; // Apply split/join rules to nodes const applySplitJoinRulesToNodes = (expectedNodes: Set, toSplit: Set, toJoin: Set, includeNeighboringCellsAsIndividualCells: boolean, workspace: Workspace) => { - const nodesToRemove = new Set(); - - expectedNodes.forEach(nodeId => { - if (!workspace.activeNeurons.has(nodeId)) { - if (toSplit.has(nodeId)) { - nodesToRemove.add(nodeId); - } else if (toJoin.has(nodeId)) { - nodesToRemove.add(nodeId); - } else if (includeNeighboringCellsAsIndividualCells && isClass(nodeId, workspace)) { - nodesToRemove.add(nodeId); - } else if (!includeNeighboringCellsAsIndividualCells && isCell(nodeId, workspace)) { - nodesToRemove.add(nodeId); - } - } - }); - - nodesToRemove.forEach(nodeId => expectedNodes.delete(nodeId)); + const nodesToRemove = new Set(); + + expectedNodes.forEach(nodeId => { + if (!workspace.activeNeurons.has(nodeId)) { + if (toSplit.has(nodeId)) { + nodesToRemove.add(nodeId); + } else if (toJoin.has(nodeId)) { + nodesToRemove.add(nodeId); + } else if (includeNeighboringCellsAsIndividualCells && isClass(nodeId, workspace)) { + nodesToRemove.add(nodeId); + } else if (!includeNeighboringCellsAsIndividualCells && isCell(nodeId, workspace)) { + nodesToRemove.add(nodeId); + } + } + }); + + nodesToRemove.forEach(nodeId => expectedNodes.delete(nodeId)); }; // Apply split/join rules to edges const applySplitJoinRulesToEdges = (expectedEdges: Set, toSplit: Set, toJoin: Set, includeNeighboringCellsAsIndividualCells: boolean, workspace: Workspace, expectedNodes: Set) => { - const edgesToRemove = new Set(); - - expectedEdges.forEach(edgeId => { - const [pre, post, type] = edgeId.split(CONNECTION_SEPARATOR); - - if ( - (!workspace.activeNeurons.has(pre) && !expectedNodes.has(pre)) || - (!workspace.activeNeurons.has(post) && !expectedNodes.has(post)) - ) { - edgesToRemove.add(edgeId); - } else if (toSplit.has(pre) || toSplit.has(post)) { - edgesToRemove.add(edgeId); - } else if (toJoin.has(pre) || toJoin.has(post)) { - edgesToRemove.add(edgeId); - } else if (includeNeighboringCellsAsIndividualCells) { - if (isClass(pre, workspace) && !workspace.activeNeurons.has(pre)) { - edgesToRemove.add(edgeId); - } - if (isClass(post, workspace) && !workspace.activeNeurons.has(post)) { - edgesToRemove.add(edgeId); - } - } else { - if (isCell(pre, workspace) && !workspace.activeNeurons.has(pre)) { - edgesToRemove.add(edgeId); - } - if (isCell(post, workspace) && !workspace.activeNeurons.has(post)) { - edgesToRemove.add(edgeId); - } - } - }); - - edgesToRemove.forEach(edgeId => expectedEdges.delete(edgeId)); + const edgesToRemove = new Set(); + + expectedEdges.forEach(edgeId => { + const [pre, post, type] = edgeId.split(CONNECTION_SEPARATOR); + + if ( + (!workspace.activeNeurons.has(pre) && !expectedNodes.has(pre)) || + (!workspace.activeNeurons.has(post) && !expectedNodes.has(post)) + ) { + edgesToRemove.add(edgeId); + } else if (toSplit.has(pre) || toSplit.has(post)) { + edgesToRemove.add(edgeId); + } else if (toJoin.has(pre) || toJoin.has(post)) { + edgesToRemove.add(edgeId); + } else if (includeNeighboringCellsAsIndividualCells) { + if (isClass(pre, workspace) && !workspace.activeNeurons.has(pre)) { + edgesToRemove.add(edgeId); + } + if (isClass(post, workspace) && !workspace.activeNeurons.has(post)) { + edgesToRemove.add(edgeId); + } + } else { + if (isCell(pre, workspace) && !workspace.activeNeurons.has(pre)) { + edgesToRemove.add(edgeId); + } + if (isCell(post, workspace) && !workspace.activeNeurons.has(post)) { + edgesToRemove.add(edgeId); + } + } + }); + + edgesToRemove.forEach(edgeId => expectedEdges.delete(edgeId)); }; -// Helper functions -const isCell = (neuronId: string, workspace: Workspace): boolean => { - const neuron = workspace.availableNeurons[neuronId]; - return neuron ? neuron.name !== neuron.nclass : false; -}; - -const isClass = (neuronId: string, workspace: Workspace): boolean => { - const neuron = workspace.availableNeurons[neuronId]; - return neuron ? neuron.name === neuron.nclass : false; -}; - -const getEdgeName = (pre: string, post: string, type: string): string => { - return `${pre}${CONNECTION_SEPARATOR}${post}${CONNECTION_SEPARATOR}${type}`; - -} \ No newline at end of file diff --git a/applications/visualizer/frontend/src/helpers/twoD/twoDHelpers.ts b/applications/visualizer/frontend/src/helpers/twoD/twoDHelpers.ts index d5cfbda2..5fd4b02e 100644 --- a/applications/visualizer/frontend/src/helpers/twoD/twoDHelpers.ts +++ b/applications/visualizer/frontend/src/helpers/twoD/twoDHelpers.ts @@ -2,6 +2,8 @@ import type { Core, ElementDefinition } from "cytoscape"; import type { Connection } from "../../rest"; import type { Workspace } from "../../models/workspace.ts"; +export const CONNECTION_SEPARATOR = '-' + export const createEdge = (conn: Connection): ElementDefinition => { return { group: "edges", @@ -107,3 +109,18 @@ export const updateHighlighted = (cy, inputIds, selectedIds, legendHighlights) = cy.elements().not(highlightedNodes).not(highlightedEdges).addClass("faded"); }; + +// Helper functions +export const isCell = (neuronId: string, workspace: Workspace): boolean => { + const neuron = workspace.availableNeurons[neuronId]; + return neuron ? neuron.name !== neuron.nclass : false; +}; + +export const isClass = (neuronId: string, workspace: Workspace): boolean => { + const neuron = workspace.availableNeurons[neuronId]; + return neuron ? neuron.name === neuron.nclass : false; +}; + +export const getEdgeName = (pre: string, post: string, type: string): string => { + return `${pre}${CONNECTION_SEPARATOR}${post}${CONNECTION_SEPARATOR}${type}`; +} \ No newline at end of file diff --git a/applications/visualizer/frontend/src/models/workspace.ts b/applications/visualizer/frontend/src/models/workspace.ts index 0c508fcf..3b0f3f0e 100644 --- a/applications/visualizer/frontend/src/models/workspace.ts +++ b/applications/visualizer/frontend/src/models/workspace.ts @@ -96,6 +96,13 @@ export class Workspace { this.updateContext(updated); } + setActiveNeurons(newActiveNeurons: Set): void { + const updated = produce(this, (draft: Workspace) => { + draft.activeNeurons = newActiveNeurons; + }); + this.updateContext(updated); + } + clearSelectedNeurons(): void { const updated = produce(this, (draft: Workspace) => { draft.selectedNeurons.clear();