diff --git a/src/charts/CytoViz/CytoViz.js b/src/charts/CytoViz/CytoViz.js index ad2f6501..6f41ed66 100644 --- a/src/charts/CytoViz/CytoViz.js +++ b/src/charts/CytoViz/CytoViz.js @@ -14,6 +14,14 @@ import { Tab, Tabs, Tooltip, + Checkbox, + TextField, + Button, + Accordion, + AccordionSummary, + AccordionDetails, + Typography, + Menu, Switch, } from '@material-ui/core'; import { @@ -21,7 +29,9 @@ import { ChevronLeft as ChevronLeftIcon, Settings as SettingsIcon, AccountTree as AccountTreeIcon, + ExpandMore as ExpandMoreIcon, } from '@material-ui/icons'; +import { Autocomplete } from '@material-ui/lab'; import CytoscapeComponent from 'react-cytoscapejs'; import cytoscape from 'cytoscape'; import BubbleSets from 'cytoscape-bubblesets'; @@ -29,12 +39,14 @@ import dagre from 'cytoscape-dagre'; import useStyles from './style'; import { ElementData, TabPanel } from './components'; import { ErrorBanner } from '../../misc'; - cytoscape.use(BubbleSets); cytoscape.use(dagre); const DEFAULT_LAYOUTS = ['dagre']; - +const initialContextMenuState = { + mouseX: null, + mouseY: null, +}; export const CytoViz = (props) => { const classes = useStyles(); const { @@ -63,6 +75,8 @@ export const CytoViz = (props) => { const [isDrawerOpen, setIsDrawerOpen] = useState(false); const [currentDrawerTab, setCurrentDrawerTab] = useState(0); const [currentElementDetails, setCurrentElementDetails] = useState(null); + const [expandedPanel, setExpandedPanel] = useState('nodeDetailsPanel'); + const closeDrawer = () => { setIsDrawerOpen(false); }; @@ -72,6 +86,9 @@ export const CytoViz = (props) => { const changeDrawerTab = (event, newValue) => { setCurrentDrawerTab(newValue); }; + const expandAccordionPanel = (panel) => (event, newExpanded) => { + setExpandedPanel(newExpanded ? panel : false); + }; // Settings const [currentLayout, setCurrentLayout] = useState(defaultSettings.layout); @@ -96,9 +113,116 @@ export const CytoViz = (props) => { const changeZoomPrecision = (event, newValue) => { setZoomPrecision(newValue); }; - // Cyto const cytoRef = useRef(null); + const [graphNodes, setGraphNodes] = useState([]); + const [selectedNodes, setSelectedNodes] = useState([]); + const [selectedNodesFieldHasError, setSelectedNodesFieldHasError] = useState(false); + const [explorationDepth, setExplorationDepth] = useState(1); + const [flowDirection, setFlowDirection] = useState({ inEdges: false, outEdges: true }); + const [childrenAreNeighbors, setChildrenAreNeighbors] = useState(true); + const [edgeClassOptions, setEdgeClassOptions] = useState([]); + const [excludedEdgeClasses, setExcludedEdgeClasses] = useState([]); + const [explorationDepthFieldHasError, setExplorationDepthFieldHasError] = useState(false); + const [positionContextMenu, setPositionContextMenu] = useState(initialContextMenuState); + const [isExplorationRunning, setIsExplorationRunning] = useState(false); + + const closeContextMenu = () => { + setPositionContextMenu(initialContextMenuState); + }; + const checkSetDepthValue = (event) => { + const newValue = event.target.value; + if (newValue.match(/^[0-9][0-9]*/)) { + setExplorationDepth(parseInt(newValue)); + setExplorationDepthFieldHasError(false); + } else if (newValue.match(/^$/)) { + setExplorationDepth(NaN); + setExplorationDepthFieldHasError(true); + } else { + setExplorationDepthFieldHasError(true); + } + }; + const launchExplore = () => { + setIsExplorationRunning(true); + closeDrawer(); + + const selectedNodesCyto = cytoRef.current.collection(); + selectedNodes.forEach((node) => { + selectedNodesCyto.merge(cytoRef.current.getElementById(node._private.data.id)); + }); + const visitedNodes = cytoRef.current.collection(); + cytoRef.current.edges().data({ asInEdgeHighlighted: false, asOutEdgeHighlighted: false }); + cytoRef.current.elements().data('hidden', true); + // remove excluded edges + let edgesToRemove = cytoRef.current.collection(); + excludedEdgeClasses.forEach((excludedEdgeClass) => + edgesToRemove.merge(cytoRef.current.elements(`edge.${excludedEdgeClass}`)) + ); + cytoRef.current.remove(edgesToRemove); + const onComplete = () => { + edgesToRemove = cytoRef.current.add(edgesToRemove); + cytoRef.current.batch(() => { + edgesToRemove.data('hidden', true); + }); + setIsExplorationRunning(false); + openDrawer(); + }; + breadthFirstExplore( + selectedNodesCyto, + visitedNodes, + explorationDepth, + flowDirection.inEdges, + flowDirection.outEdges, + onComplete + ); + }; + const breadthFirstExplore = (startingNodes, visitedNodes, depth, followInEdges, followOutEdges, onComplete) => { + let neighbors = cytoRef.current.collection(); // get an empty collection + startingNodes.forEach((node) => { + if (followInEdges) { + neighbors.merge(node.incomers('node')); + neighbors.merge(node.parent()); + } + if (followOutEdges) { + neighbors.merge(node.outgoers('node')); + neighbors.merge(node.children()); + } + if (childrenAreNeighbors) { + neighbors.merge(node.parent().children()); + } + }); + startingNodes.data('hidden', false); + startingNodes.parent().data('hidden', false); // otherwise the element wont be shown + visitedNodes.edgesWith(visitedNodes).data('hidden', false); + startingNodes.edgesWith(visitedNodes).data('hidden', false); + + visitedNodes.merge(startingNodes); // mark all visited nodes + neighbors = neighbors.subtract(visitedNodes); // do not put already discovered nodes in the next searchrun + + cytoRef.current.animate({ + fit: { eles: startingNodes }, + center: { eles: startingNodes }, + duration: 1000, + complete: depth === 0 ? onComplete() : {}, + }); + if (depth < 1) { + return; + } + // to reduce maximal runtime + if (depth > 1 && neighbors.length === 0) { + depth = 1; + } + setTimeout(() => { + breadthFirstExplore(neighbors, visitedNodes, depth - 1, followInEdges, followOutEdges, onComplete); + }, 1500); + }; + const getEdgesClasses = (cytoscapeRef) => { + const edgeClassesSet = new Set(); + cytoscapeRef.edges().forEach((edge) => { + edge.classes().forEach((currentClass) => edgeClassesSet.add(currentClass)); + }); + return Array.from(edgeClassesSet); + }; useEffect(() => { Object.values(extraLayouts).forEach((layout) => { @@ -113,6 +237,8 @@ export const CytoViz = (props) => { return; } cytoRef.current = cytoscapeRef; + setGraphNodes(cytoscapeRef.nodes().toArray()); + setEdgeClassOptions(getEdgesClasses(cytoscapeRef)); cytoscapeRef.removeAllListeners(); cytoscapeRef.elements().removeAllListeners(); // Prevent multiple selection & init elements selection behavior @@ -122,6 +248,7 @@ export const CytoViz = (props) => { selectedElement.select(); selectedElement.outgoers('edge').data('asOutEdgeHighlighted', true); selectedElement.incomers('edge').data('asInEdgeHighlighted', true); + selectedElement.neighborhood().data('hidden', false); setCurrentElementDetails(getElementDetailsCallback(selectedElement)); }); cytoscapeRef.on('unselect', 'node, edge', function (e) { @@ -131,12 +258,26 @@ export const CytoViz = (props) => { // Add handling of double click events cytoscapeRef.on('dbltap', 'node, edge', function (e) { const selectedElement = e.target; + selectedElement.neighborhood().data('hidden', false); if (selectedElement.selectable()) { - setCurrentDrawerTab(0); - setIsDrawerOpen(true); + openDrawer(); + setExpandedPanel('nodeDetailsPanel'); setCurrentElementDetails(getElementDetailsCallback(selectedElement)); } }); + cytoscapeRef.on('cxttap', function (e) { + if (cytoscapeRef.nodes('node:selected').length > 0) { + e.preventDefault(); + setPositionContextMenu({ + mouseX: e.originalEvent.clientX - 2, + mouseY: e.originalEvent.clientY - 4, + }); + } + }); + cytoscapeRef.on('click', function (e) { + closeContextMenu(); + }); + // Init bubblesets const bb = cytoscapeRef.bubbleSets(); for (const groupName in bubblesets) { @@ -226,8 +367,181 @@ export const CytoViz = (props) => {
{labels_.accordion.exploreGraph.flowDirection}
+ {!(flowDirection.inEdges || flowDirection.outEdges) && ( +