Skip to content

Commit 1c6a520

Browse files
author
Oskar Howe
committed
feat: collapsable compounds, react switch, layout change not applied when open node with right click
1 parent 8a82fdd commit 1c6a520

File tree

4 files changed

+152
-37
lines changed

4 files changed

+152
-37
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
"cytoscape": "^3.20.0",
7272
"cytoscape-bubblesets": "^3.1.0",
7373
"cytoscape-dagre": "^2.4.0",
74+
"cytoscape-expand-collapse": "^4.1.0",
7475
"cytoscape-layers": "^2.2.0",
7576
"date-fns": "^2.22.1",
7677
"powerbi-client-react": "^1.3.3",

src/charts/CytoViz/CytoViz.js

Lines changed: 130 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,20 @@
11
// Copyright (c) Cosmo Tech.
22
// Licensed under the MIT license.
33

4-
import React, { useEffect, useState } from 'react';
4+
import React, { useEffect, useRef, useState } from 'react';
55
import PropTypes from 'prop-types';
6-
import { Checkbox, CircularProgress, Drawer, IconButton, MenuItem, Select, Slider, Tabs, Tab } from '@material-ui/core';
6+
import {
7+
Checkbox,
8+
CircularProgress,
9+
Drawer,
10+
IconButton,
11+
MenuItem,
12+
Select,
13+
Slider,
14+
Tabs,
15+
Tab,
16+
Switch,
17+
} from '@material-ui/core';
718
import {
819
ChevronRight as ChevronRightIcon,
920
ChevronLeft as ChevronLeftIcon,
@@ -14,15 +25,48 @@ import CytoscapeComponent from 'react-cytoscapejs';
1425
import cytoscape from 'cytoscape';
1526
import BubbleSets from 'cytoscape-bubblesets';
1627
import dagre from 'cytoscape-dagre';
28+
import expandCollapse from 'cytoscape-expand-collapse';
1729
import useStyles from './style';
1830
import { ElementData, TabPanel } from './components';
1931
import { ErrorBanner } from '../../misc';
2032

2133
cytoscape.use(BubbleSets);
2234
cytoscape.use(dagre);
35+
if (typeof cytoscape('core', 'expandCollapse') === 'undefined') {
36+
cytoscape.use(expandCollapse);
37+
}
2338

2439
const DEFAULT_LAYOUTS = ['dagre'];
25-
40+
const getCompoundApiOptions = (currentLayout, useCompactMode, spacingFactor) => ({
41+
layoutBy: {
42+
name: currentLayout,
43+
nodeDimensionsIncludeLabels: !useCompactMode,
44+
spacingFactor: spacingFactor,
45+
}, // to rearrange after expand/collapse. It's just layout options or whole layout function. Choose your side!
46+
// recommended usage: use cose-bilkent layout with randomize: false to preserve mental map upon expand/collapse
47+
fisheye: false, // whether to perform fisheye view after expand/collapse you can specify a function too
48+
animate: true, // whether to animate on drawing changes you can specify a function too
49+
animationDuration: 1000, // when animate is true, the duration in milliseconds of the animation
50+
ready: function () {}, // callback when expand/collapse initialized
51+
undoable: true, // and if undoRedoExtension exists,
52+
cueEnabled: false, // Whether cues are enabled
53+
expandCollapseCuePosition: 'top-left', // default cue position is top left you can specify a function per node too
54+
expandCollapseCueSize: 12, // size of expand-collapse cue
55+
expandCollapseCueLineSize: 8, // size of lines used for drawing plus-minus icons
56+
expandCueImage: undefined, // image of expand icon if undefined draw regular expand cue
57+
collapseCueImage: undefined, // image of collapse icon if undefined draw regular collapse cue
58+
expandCollapseCueSensitivity: 1, // sensitivity of expand-collapse cues
59+
// edgeTypeInfo: 'edgeType',
60+
// the name of the field that has the edge type, retrieved from edge.data(), can be a function,
61+
// if reading the field returns undefined the collapsed edge type will be "unknown"
62+
groupEdgesOfSameTypeOnCollapse: false, // if true, the edges to be collapsed will be grouped according to their types
63+
// the created collapsed edges will have same type as their group.
64+
// if false the collapased edge will have "unknown" type.
65+
allowNestedEdgeCollapse: false,
66+
// when you want to collapse a compound edge (edge which contains other edges) and normal edge,
67+
// should it collapse without expanding the compound first
68+
zIndex: 0, // z-index value of the canvas in which cue ımages are drawn
69+
});
2670
export const CytoViz = (props) => {
2771
const classes = useStyles();
2872
const {
@@ -69,6 +113,8 @@ export const CytoViz = (props) => {
69113
Math.log10(defaultSettings.minZoom),
70114
Math.log10(defaultSettings.maxZoom),
71115
]);
116+
const [allCompoundsAreCollapsed, setAllCompoundsAreCollapsed] = useState(defaultSettings.collapseAll);
117+
72118
const changeCurrentLayout = (event) => {
73119
setCurrentLayout(event.target.value);
74120
};
@@ -84,6 +130,23 @@ export const CytoViz = (props) => {
84130
const changeZoomPrecision = (event, newValue) => {
85131
setZoomPrecision(newValue);
86132
};
133+
const toggleAllNodesCollapsed = (event) => {
134+
if (compoundsApiRef.current && cytoRef.current) {
135+
if (!allCompoundsAreCollapsed) {
136+
compoundsApiRef.current.collapseAll(getCompoundApiOptions(currentLayout, useCompactMode, spacingFactor));
137+
compoundsApiRef.current.collapseAllEdges();
138+
setAllCompoundsAreCollapsed(true);
139+
} else {
140+
compoundsApiRef.current.expandAllEdges();
141+
compoundsApiRef.current.expandAll(getCompoundApiOptions(currentLayout, useCompactMode, spacingFactor));
142+
setAllCompoundsAreCollapsed(false);
143+
}
144+
}
145+
};
146+
147+
// Cyto
148+
const compoundsApiRef = useRef(null);
149+
const cytoRef = useRef(null);
87150

88151
useEffect(() => {
89152
Object.values(extraLayouts).forEach((layout) => {
@@ -94,41 +157,63 @@ export const CytoViz = (props) => {
94157
}, [extraLayouts]);
95158

96159
const initCytoscape = (cytoscapeRef) => {
97-
cytoscapeRef.removeAllListeners();
98-
// Prevent multiple selection & init elements selection behavior
99-
cytoscapeRef.on('select', 'node, edge', function (e) {
100-
cytoscapeRef.elements().not(e.target).unselect();
101-
const selectedElement = e.target;
102-
setCurrentElementDetails(getElementDetailsCallback(selectedElement));
103-
});
104-
cytoscapeRef.on('unselect', 'node, edge', function (e) {
105-
setCurrentElementDetails(null);
106-
});
107-
// Add handling of double click events
108-
cytoscapeRef.on('dbltap', 'node, edge', function (e) {
109-
const selectedElement = e.target;
110-
if (selectedElement.selectable()) {
111-
setCurrentDrawerTab(0);
112-
setIsDrawerOpen(true);
160+
if (!cytoRef.current || cytoRef.current !== cytoscapeRef) {
161+
cytoRef.current = cytoscapeRef;
162+
cytoRef.current.removeAllListeners();
163+
cytoRef.current.elements().removeAllListeners();
164+
// Prevent multiple selection & init elements selection behavior
165+
compoundsApiRef.current = cytoRef.current.expandCollapse(
166+
getCompoundApiOptions(currentLayout, useCompactMode, spacingFactor)
167+
);
168+
cytoRef.current.on('select', 'node, edge', function (e) {
169+
cytoRef.current.elements().not(e.target).unselect();
170+
const selectedElement = e.target;
113171
setCurrentElementDetails(getElementDetailsCallback(selectedElement));
114-
}
115-
});
116-
117-
// Init bubblesets
118-
const bb = cytoscapeRef.bubbleSets();
119-
for (const groupName in bubblesets) {
120-
const nodesGroup = cytoscapeRef.nodes(`.${groupName}`);
121-
const groupColor = bubblesets[groupName];
122-
bb.addPath(nodesGroup, undefined, null, {
123-
virtualEdges: true,
124-
style: {
125-
fill: groupColor,
126-
stroke: groupColor,
127-
},
128172
});
173+
cytoRef.current.on('unselect', 'node, edge', function (e) {
174+
setCurrentElementDetails(null);
175+
});
176+
// Add handling of double click events
177+
cytoRef.current.on('dbltap', 'node, edge', function (e) {
178+
const selectedElement = e.target;
179+
if (selectedElement.selectable()) {
180+
setCurrentDrawerTab(0);
181+
setIsDrawerOpen(true);
182+
setCurrentElementDetails(getElementDetailsCallback(selectedElement));
183+
}
184+
});
185+
cytoRef.current.on('cxttap', 'node.cy-expand-collapse-collapsed-node', function (e) {
186+
const selectedElement = e.target;
187+
// needs to be done this way because of know bugs when combining node and edge expand collapse methods:
188+
// https://github.com/iVis-at-Bilkent/cytoscape.js-expand-collapse/issues/100
189+
selectedElement
190+
.neighborhood('node')
191+
.forEach((neighbor) => compoundsApiRef.current.expandEdgesBetweenNodes([selectedElement, neighbor]));
192+
compoundsApiRef.current.expand(
193+
selectedElement,
194+
getCompoundApiOptions(currentLayout, useCompactMode, spacingFactor)
195+
);
196+
setAllCompoundsAreCollapsed(false);
197+
});
198+
if (allCompoundsAreCollapsed) {
199+
compoundsApiRef.current.collapseAll(getCompoundApiOptions(currentLayout, useCompactMode, spacingFactor));
200+
compoundsApiRef.current.collapseAllEdges();
201+
}
202+
// Init bubblesets
203+
const bb = cytoRef.current.bubbleSets();
204+
for (const groupName in bubblesets) {
205+
const nodesGroup = cytoRef.current.nodes(`.${groupName}`);
206+
const groupColor = bubblesets[groupName];
207+
bb.addPath(nodesGroup, undefined, null, {
208+
virtualEdges: true,
209+
style: {
210+
fill: groupColor,
211+
stroke: groupColor,
212+
},
213+
});
214+
}
129215
}
130216
};
131-
132217
const errorBanner = error && <ErrorBanner error={error} labels={labels_.errorBanner} />;
133218
const loadingPlaceholder = loading && !error && !placeholderMessage && (
134219
<div data-cy="cytoviz-loading-container" className={classes.loadingContainer}>
@@ -146,7 +231,7 @@ export const CytoViz = (props) => {
146231
<>
147232
<CytoscapeComponent
148233
id="cytoviz-cytoscape-scene" // Component does not forward data-cy prop, use id instead
149-
cy={initCytoscape}
234+
cy={(cy) => initCytoscape(cy)}
150235
stylesheet={cytoscapeStylesheet}
151236
className={classes.cytoscapeContainer}
152237
elements={elements}
@@ -225,9 +310,15 @@ export const CytoViz = (props) => {
225310
</Select>
226311
</div>
227312
</div>
313+
<div className={classes.settingLine}>
314+
<div className={classes.settingLabel}>{labels_.settings.collapse}</div>
315+
<div className={classes.settingInputContainer}>
316+
<Switch color="primary" checked={allCompoundsAreCollapsed} onChange={toggleAllNodesCollapsed} />
317+
</div>
318+
</div>
228319
<div className={classes.settingLine}>
229320
<div className={classes.settingLabel} onClick={toggleUseCompactMode}>
230-
{labels.settings.compactMode}
321+
{labels_.settings.compactMode}
231322
</div>
232323
<div className={classes.settingInputContainer}>
233324
<Checkbox
@@ -303,6 +394,7 @@ CytoViz.propTypes = {
303394
maxZoom: PropTypes.number,
304395
useCompactMode: PropTypes.bool,
305396
spacingFactor: PropTypes.number,
397+
collapseAll: PropTypes.bool,
306398
}),
307399
/**
308400
* Array of cytoscape elements to display
@@ -379,6 +471,7 @@ const DEFAULT_LABELS = {
379471
title: 'Settings',
380472
spacingFactor: 'Spacing factor',
381473
zoomLimits: 'Min & max zoom',
474+
collapse: 'Compound nodes collapsed (Right click to open)',
382475
},
383476
elementData: {
384477
dictKey: 'Key',
@@ -394,6 +487,7 @@ CytoViz.defaultProps = {
394487
maxZoom: 1,
395488
useCompactMode: true,
396489
spacingFactor: 1,
490+
collapseAll: false,
397491
},
398492
extraLayouts: {},
399493
groups: {},

src/charts/CytoViz/components/ElementData/ElementData.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,22 @@ import useStyles from './style';
77

88
const _generateAttributeDetails = (classes, labels, attributeName, attributeValue) => {
99
const attributeLabel = labels?.attributes?.[attributeName] || attributeName;
10-
const attributesToIgnore = ['label', 'Label', 'parent', 'source', 'target'];
10+
const attributesToIgnore = [
11+
'label',
12+
'Label',
13+
'parent',
14+
'source',
15+
'target',
16+
'collapse', // attributes from expand-collapse extension
17+
'collapsedChildren', // attributes from expand-collapse extension
18+
'position-before-collapse', // attributes from expand-collapse extension
19+
'size-before-collapse', // attributes from expand-collapse extension
20+
'x-before-fisheye',
21+
'y-before-fisheye',
22+
'width-before-fisheye',
23+
'height-before-fisheye',
24+
'infoLabel',
25+
];
1126
if (attributesToIgnore.indexOf(attributeName) !== -1) {
1227
return null;
1328
}

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1677,6 +1677,11 @@ cytoscape-dagre@^2.4.0:
16771677
dependencies:
16781678
dagre "^0.8.5"
16791679

1680+
cytoscape-expand-collapse@^4.1.0:
1681+
version "4.1.0"
1682+
resolved "https://registry.yarnpkg.com/cytoscape-expand-collapse/-/cytoscape-expand-collapse-4.1.0.tgz#bfcd57692fae4585a112d94843790da38c3301a8"
1683+
integrity sha512-RJsoSDAC0ui6U6CeM7z2tiFzsk4Z2aPG8PgNIwKeFVykFmMr5S+5aT1YceQZ5XnwGFGH0uteZ96XyAfUAVb/fw==
1684+
16801685
cytoscape-layers@^2.2.0:
16811686
version "2.2.0"
16821687
resolved "https://registry.yarnpkg.com/cytoscape-layers/-/cytoscape-layers-2.2.0.tgz#ac0251dd06c2e1b5d5c341687d88cc6ac5032378"

0 commit comments

Comments
 (0)