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' ;
55import 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' ;
718import {
819 ChevronRight as ChevronRightIcon ,
920 ChevronLeft as ChevronLeftIcon ,
@@ -14,15 +25,48 @@ import CytoscapeComponent from 'react-cytoscapejs';
1425import cytoscape from 'cytoscape' ;
1526import BubbleSets from 'cytoscape-bubblesets' ;
1627import dagre from 'cytoscape-dagre' ;
28+ import expandCollapse from 'cytoscape-expand-collapse' ;
1729import useStyles from './style' ;
1830import { ElementData , TabPanel } from './components' ;
1931import { ErrorBanner } from '../../misc' ;
2032
2133cytoscape . use ( BubbleSets ) ;
2234cytoscape . use ( dagre ) ;
35+ if ( typeof cytoscape ( 'core' , 'expandCollapse' ) === 'undefined' ) {
36+ cytoscape . use ( expandCollapse ) ;
37+ }
2338
2439const 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+ } ) ;
2670export 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 . collapseAllCompounds ) ;
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,34 +157,59 @@ export const CytoViz = (props) => {
94157 } , [ extraLayouts ] ) ;
95158
96159 const initCytoscape = ( cytoscapeRef ) => {
97- cytoscapeRef . removeAllListeners ( ) ;
160+ if ( cytoRef . current != null && cytoRef . current === cytoscapeRef ) {
161+ return ;
162+ }
163+ cytoRef . current = cytoscapeRef ;
164+ cytoRef . current . removeAllListeners ( ) ;
165+ cytoRef . current . elements ( ) . removeAllListeners ( ) ;
98166 // Prevent multiple selection & init elements selection behavior
99- cytoscapeRef . on ( 'select' , 'node, edge' , function ( e ) {
100- cytoscapeRef . edges ( ) . data ( { asInEdgeHighlighted : false , asOutEdgeHighlighted : false } ) ;
167+ compoundsApiRef . current = cytoRef . current . expandCollapse (
168+ getCompoundApiOptions ( currentLayout , useCompactMode , spacingFactor )
169+ ) ;
170+ if ( allCompoundsAreCollapsed ) {
171+ compoundsApiRef . current . collapseAll ( getCompoundApiOptions ( currentLayout , useCompactMode , spacingFactor ) ) ;
172+ compoundsApiRef . current . collapseAllEdges ( ) ;
173+ }
174+
175+ cytoRef . current . on ( 'select' , 'node, edge' , function ( e ) {
176+ cytoRef . current . edges ( ) . data ( { asInEdgeHighlighted : false , asOutEdgeHighlighted : false } ) ;
101177 const selectedElement = e . target ;
102178 selectedElement . select ( ) ;
103179 selectedElement . outgoers ( 'edge' ) . data ( 'asOutEdgeHighlighted' , true ) ;
104180 selectedElement . incomers ( 'edge' ) . data ( 'asInEdgeHighlighted' , true ) ;
105181 setCurrentElementDetails ( getElementDetailsCallback ( selectedElement ) ) ;
106182 } ) ;
107- cytoscapeRef . on ( 'unselect' , 'node, edge' , function ( e ) {
108- cytoscapeRef . edges ( ) . data ( { asInEdgeHighlighted : false , asOutEdgeHighlighted : false } ) ;
183+ cytoRef . current . on ( 'unselect' , 'node, edge' , function ( e ) {
184+ cytoRef . current . edges ( ) . data ( { asInEdgeHighlighted : false , asOutEdgeHighlighted : false } ) ;
109185 setCurrentElementDetails ( null ) ;
110186 } ) ;
111187 // Add handling of double click events
112- cytoscapeRef . on ( 'dbltap' , 'node, edge' , function ( e ) {
188+ cytoRef . current . on ( 'dbltap' , 'node, edge' , function ( e ) {
113189 const selectedElement = e . target ;
114190 if ( selectedElement . selectable ( ) ) {
115191 setCurrentDrawerTab ( 0 ) ;
116192 setIsDrawerOpen ( true ) ;
117193 setCurrentElementDetails ( getElementDetailsCallback ( selectedElement ) ) ;
118194 }
119195 } ) ;
120-
196+ cytoRef . current . on ( 'cxttap' , 'node.cy-expand-collapse-collapsed-node' , function ( e ) {
197+ const selectedElement = e . target ;
198+ // needs to be done this way because of know bugs when combining node and edge expand collapse methods:
199+ // https://github.com/iVis-at-Bilkent/cytoscape.js-expand-collapse/issues/100
200+ selectedElement
201+ . neighborhood ( 'node' )
202+ . forEach ( ( neighbor ) => compoundsApiRef . current . expandEdgesBetweenNodes ( [ selectedElement , neighbor ] ) ) ;
203+ compoundsApiRef . current . expand (
204+ selectedElement ,
205+ getCompoundApiOptions ( currentLayout , useCompactMode , spacingFactor )
206+ ) ;
207+ setAllCompoundsAreCollapsed ( false ) ;
208+ } ) ;
121209 // Init bubblesets
122- const bb = cytoscapeRef . bubbleSets ( ) ;
210+ const bb = cytoRef . current . bubbleSets ( ) ;
123211 for ( const groupName in bubblesets ) {
124- const nodesGroup = cytoscapeRef . nodes ( `.${ groupName } ` ) ;
212+ const nodesGroup = cytoRef . current . nodes ( `.${ groupName } ` ) ;
125213 const groupColor = bubblesets [ groupName ] ;
126214 bb . addPath ( nodesGroup , undefined , null , {
127215 virtualEdges : true ,
@@ -132,7 +220,6 @@ export const CytoViz = (props) => {
132220 } ) ;
133221 }
134222 } ;
135-
136223 const errorBanner = error && < ErrorBanner error = { error } labels = { labels_ . errorBanner } /> ;
137224 const loadingPlaceholder = loading && ! error && ! placeholderMessage && (
138225 < div data-cy = "cytoviz-loading-container" className = { classes . loadingContainer } >
@@ -150,7 +237,7 @@ export const CytoViz = (props) => {
150237 < >
151238 < CytoscapeComponent
152239 id = "cytoviz-cytoscape-scene" // Component does not forward data-cy prop, use id instead
153- cy = { initCytoscape }
240+ cy = { ( cy ) => initCytoscape ( cy ) }
154241 stylesheet = { cytoscapeStylesheet }
155242 className = { classes . cytoscapeContainer }
156243 elements = { elements }
@@ -229,9 +316,15 @@ export const CytoViz = (props) => {
229316 </ Select >
230317 </ div >
231318 </ div >
319+ < div className = { classes . settingLine } >
320+ < div className = { classes . settingLabel } > { labels_ . settings . collapse } </ div >
321+ < div className = { classes . settingInputContainer } >
322+ < Switch color = "primary" checked = { allCompoundsAreCollapsed } onChange = { toggleAllNodesCollapsed } />
323+ </ div >
324+ </ div >
232325 < div className = { classes . settingLine } >
233326 < div className = { classes . settingLabel } onClick = { toggleUseCompactMode } >
234- { labels . settings . compactMode }
327+ { labels_ . settings . compactMode }
235328 </ div >
236329 < div className = { classes . settingInputContainer } >
237330 < Checkbox
@@ -307,6 +400,7 @@ CytoViz.propTypes = {
307400 maxZoom : PropTypes . number ,
308401 useCompactMode : PropTypes . bool ,
309402 spacingFactor : PropTypes . number ,
403+ collapseAllCompounds : PropTypes . bool ,
310404 } ) ,
311405 /**
312406 * Array of cytoscape elements to display
@@ -383,6 +477,7 @@ const DEFAULT_LABELS = {
383477 title : 'Settings' ,
384478 spacingFactor : 'Spacing factor' ,
385479 zoomLimits : 'Min & max zoom' ,
480+ collapse : 'Compound nodes collapsed (Right click to open)' ,
386481 } ,
387482 elementData : {
388483 dictKey : 'Key' ,
@@ -398,6 +493,7 @@ CytoViz.defaultProps = {
398493 maxZoom : 1 ,
399494 useCompactMode : true ,
400495 spacingFactor : 1 ,
496+ collapseAllCompounds : false ,
401497 } ,
402498 extraLayouts : { } ,
403499 groups : { } ,
0 commit comments