diff --git a/package.json b/package.json
index b1f8d50c..af9bfe88 100644
--- a/package.json
+++ b/package.json
@@ -71,6 +71,7 @@
"cytoscape": "^3.20.0",
"cytoscape-bubblesets": "^3.1.0",
"cytoscape-dagre": "^2.4.0",
+ "cytoscape-expand-collapse": "^4.1.0",
"cytoscape-layers": "^2.2.0",
"date-fns": "^2.22.1",
"powerbi-client-react": "^1.3.3",
diff --git a/src/charts/CytoViz/CytoViz.js b/src/charts/CytoViz/CytoViz.js
index ea11f59a..42a94fca 100644
--- a/src/charts/CytoViz/CytoViz.js
+++ b/src/charts/CytoViz/CytoViz.js
@@ -1,9 +1,20 @@
// Copyright (c) Cosmo Tech.
// Licensed under the MIT license.
-import React, { useEffect, useState } from 'react';
+import React, { useEffect, useRef, useState } from 'react';
import PropTypes from 'prop-types';
-import { Checkbox, CircularProgress, Drawer, IconButton, MenuItem, Select, Slider, Tabs, Tab } from '@material-ui/core';
+import {
+ Checkbox,
+ CircularProgress,
+ Drawer,
+ IconButton,
+ MenuItem,
+ Select,
+ Slider,
+ Tabs,
+ Tab,
+ Switch,
+} from '@material-ui/core';
import {
ChevronRight as ChevronRightIcon,
ChevronLeft as ChevronLeftIcon,
@@ -14,15 +25,48 @@ import CytoscapeComponent from 'react-cytoscapejs';
import cytoscape from 'cytoscape';
import BubbleSets from 'cytoscape-bubblesets';
import dagre from 'cytoscape-dagre';
+import expandCollapse from 'cytoscape-expand-collapse';
import useStyles from './style';
import { ElementData, TabPanel } from './components';
import { ErrorBanner } from '../../misc';
cytoscape.use(BubbleSets);
cytoscape.use(dagre);
+if (typeof cytoscape('core', 'expandCollapse') === 'undefined') {
+ cytoscape.use(expandCollapse);
+}
const DEFAULT_LAYOUTS = ['dagre'];
-
+const getCompoundApiOptions = (currentLayout, useCompactMode, spacingFactor) => ({
+ layoutBy: {
+ name: currentLayout,
+ nodeDimensionsIncludeLabels: !useCompactMode,
+ spacingFactor: spacingFactor,
+ }, // to rearrange after expand/collapse. It's just layout options or whole layout function. Choose your side!
+ // recommended usage: use cose-bilkent layout with randomize: false to preserve mental map upon expand/collapse
+ fisheye: false, // whether to perform fisheye view after expand/collapse you can specify a function too
+ animate: true, // whether to animate on drawing changes you can specify a function too
+ animationDuration: 1000, // when animate is true, the duration in milliseconds of the animation
+ ready: function () {}, // callback when expand/collapse initialized
+ undoable: true, // and if undoRedoExtension exists,
+ cueEnabled: false, // Whether cues are enabled
+ expandCollapseCuePosition: 'top-left', // default cue position is top left you can specify a function per node too
+ expandCollapseCueSize: 12, // size of expand-collapse cue
+ expandCollapseCueLineSize: 8, // size of lines used for drawing plus-minus icons
+ expandCueImage: undefined, // image of expand icon if undefined draw regular expand cue
+ collapseCueImage: undefined, // image of collapse icon if undefined draw regular collapse cue
+ expandCollapseCueSensitivity: 1, // sensitivity of expand-collapse cues
+ // edgeTypeInfo: 'edgeType',
+ // the name of the field that has the edge type, retrieved from edge.data(), can be a function,
+ // if reading the field returns undefined the collapsed edge type will be "unknown"
+ groupEdgesOfSameTypeOnCollapse: false, // if true, the edges to be collapsed will be grouped according to their types
+ // the created collapsed edges will have same type as their group.
+ // if false the collapased edge will have "unknown" type.
+ allowNestedEdgeCollapse: false,
+ // when you want to collapse a compound edge (edge which contains other edges) and normal edge,
+ // should it collapse without expanding the compound first
+ zIndex: 0, // z-index value of the canvas in which cue ımages are drawn
+});
export const CytoViz = (props) => {
const classes = useStyles();
const {
@@ -69,6 +113,8 @@ export const CytoViz = (props) => {
Math.log10(defaultSettings.minZoom),
Math.log10(defaultSettings.maxZoom),
]);
+ const [allCompoundsAreCollapsed, setAllCompoundsAreCollapsed] = useState(defaultSettings.collapseAllCompounds);
+
const changeCurrentLayout = (event) => {
setCurrentLayout(event.target.value);
};
@@ -84,6 +130,23 @@ export const CytoViz = (props) => {
const changeZoomPrecision = (event, newValue) => {
setZoomPrecision(newValue);
};
+ const toggleAllNodesCollapsed = (event) => {
+ if (compoundsApiRef.current && cytoRef.current) {
+ if (!allCompoundsAreCollapsed) {
+ compoundsApiRef.current.collapseAll(getCompoundApiOptions(currentLayout, useCompactMode, spacingFactor));
+ compoundsApiRef.current.collapseAllEdges();
+ setAllCompoundsAreCollapsed(true);
+ } else {
+ compoundsApiRef.current.expandAllEdges();
+ compoundsApiRef.current.expandAll(getCompoundApiOptions(currentLayout, useCompactMode, spacingFactor));
+ setAllCompoundsAreCollapsed(false);
+ }
+ }
+ };
+
+ // Cyto
+ const compoundsApiRef = useRef(null);
+ const cytoRef = useRef(null);
useEffect(() => {
Object.values(extraLayouts).forEach((layout) => {
@@ -94,22 +157,35 @@ export const CytoViz = (props) => {
}, [extraLayouts]);
const initCytoscape = (cytoscapeRef) => {
- cytoscapeRef.removeAllListeners();
+ if (cytoRef.current != null && cytoRef.current === cytoscapeRef) {
+ return;
+ }
+ cytoRef.current = cytoscapeRef;
+ cytoRef.current.removeAllListeners();
+ cytoRef.current.elements().removeAllListeners();
// Prevent multiple selection & init elements selection behavior
- cytoscapeRef.on('select', 'node, edge', function (e) {
- cytoscapeRef.edges().data({ asInEdgeHighlighted: false, asOutEdgeHighlighted: false });
+ compoundsApiRef.current = cytoRef.current.expandCollapse(
+ getCompoundApiOptions(currentLayout, useCompactMode, spacingFactor)
+ );
+ if (allCompoundsAreCollapsed) {
+ compoundsApiRef.current.collapseAll(getCompoundApiOptions(currentLayout, useCompactMode, spacingFactor));
+ compoundsApiRef.current.collapseAllEdges();
+ }
+
+ cytoRef.current.on('select', 'node, edge', function (e) {
+ cytoRef.current.edges().data({ asInEdgeHighlighted: false, asOutEdgeHighlighted: false });
const selectedElement = e.target;
selectedElement.select();
selectedElement.outgoers('edge').data('asOutEdgeHighlighted', true);
selectedElement.incomers('edge').data('asInEdgeHighlighted', true);
setCurrentElementDetails(getElementDetailsCallback(selectedElement));
});
- cytoscapeRef.on('unselect', 'node, edge', function (e) {
- cytoscapeRef.edges().data({ asInEdgeHighlighted: false, asOutEdgeHighlighted: false });
+ cytoRef.current.on('unselect', 'node, edge', function (e) {
+ cytoRef.current.edges().data({ asInEdgeHighlighted: false, asOutEdgeHighlighted: false });
setCurrentElementDetails(null);
});
// Add handling of double click events
- cytoscapeRef.on('dbltap', 'node, edge', function (e) {
+ cytoRef.current.on('dbltap', 'node, edge', function (e) {
const selectedElement = e.target;
if (selectedElement.selectable()) {
setCurrentDrawerTab(0);
@@ -117,11 +193,23 @@ export const CytoViz = (props) => {
setCurrentElementDetails(getElementDetailsCallback(selectedElement));
}
});
-
+ cytoRef.current.on('cxttap', 'node.cy-expand-collapse-collapsed-node', function (e) {
+ const selectedElement = e.target;
+ // needs to be done this way because of know bugs when combining node and edge expand collapse methods:
+ // https://github.com/iVis-at-Bilkent/cytoscape.js-expand-collapse/issues/100
+ selectedElement
+ .neighborhood('node')
+ .forEach((neighbor) => compoundsApiRef.current.expandEdgesBetweenNodes([selectedElement, neighbor]));
+ compoundsApiRef.current.expand(
+ selectedElement,
+ getCompoundApiOptions(currentLayout, useCompactMode, spacingFactor)
+ );
+ setAllCompoundsAreCollapsed(false);
+ });
// Init bubblesets
- const bb = cytoscapeRef.bubbleSets();
+ const bb = cytoRef.current.bubbleSets();
for (const groupName in bubblesets) {
- const nodesGroup = cytoscapeRef.nodes(`.${groupName}`);
+ const nodesGroup = cytoRef.current.nodes(`.${groupName}`);
const groupColor = bubblesets[groupName];
bb.addPath(nodesGroup, undefined, null, {
virtualEdges: true,
@@ -132,7 +220,6 @@ export const CytoViz = (props) => {
});
}
};
-
const errorBanner = error &&