Skip to content

Commit 8b08621

Browse files
author
Xing
authored
Remove selected nodes after they are deleted (#49)
* Fix #45 * Fix PEP8
1 parent 0d2b47f commit 8b08621

File tree

6 files changed

+194
-53
lines changed

6 files changed

+194
-53
lines changed

dash_cytoscape/dash_cytoscape.dev.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dash_cytoscape/dash_cytoscape.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dash_cytoscape/dash_cytoscape_extra.dev.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dash_cytoscape/dash_cytoscape_extra.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import json
2+
import os
3+
import random
4+
5+
import dash
6+
from dash.dependencies import Input, Output, State
7+
import dash_core_components as dcc
8+
import dash_html_components as html
9+
10+
import dash_cytoscape as cyto
11+
12+
asset_path = os.path.join(
13+
os.path.dirname(os.path.abspath(__file__)),
14+
'..', 'assets'
15+
)
16+
17+
app = dash.Dash(__name__, assets_folder=asset_path)
18+
server = app.server
19+
20+
random.seed(2019)
21+
22+
nodes = [
23+
{'data': {'id': str(i), 'label': 'Node {}'.format(i)}}
24+
for i in range(1, 21)
25+
]
26+
27+
edges = [
28+
{'data': {'source': str(random.randint(1, 20)), 'target': str(random.randint(1, 20))}}
29+
for _ in range(30)
30+
]
31+
32+
default_elements = nodes + edges
33+
34+
styles = {
35+
'json-output': {
36+
'overflow-y': 'scroll',
37+
'height': 'calc(50% - 25px)',
38+
'border': 'thin lightgrey solid'
39+
},
40+
'tab': {'height': 'calc(98vh - 115px)'}
41+
}
42+
43+
app.layout = html.Div([
44+
html.Div(className='eight columns', children=[
45+
cyto.Cytoscape(
46+
id='cytoscape',
47+
elements=default_elements,
48+
layout={
49+
'name': 'grid'
50+
},
51+
style={
52+
'height': '95vh',
53+
'width': '100%'
54+
}
55+
)
56+
]),
57+
58+
html.Div(className='four columns', children=[
59+
dcc.Tabs(id='tabs', children=[
60+
dcc.Tab(label='Actions', children=[
61+
html.Button("Remove Selected Node", id='remove-button')
62+
]),
63+
dcc.Tab(label='Tap Data', children=[
64+
html.Div(style=styles['tab'], children=[
65+
html.P('Node Data JSON:'),
66+
html.Pre(
67+
id='tap-node-data-json-output',
68+
style=styles['json-output']
69+
),
70+
html.P('Edge Data JSON:'),
71+
html.Pre(
72+
id='tap-edge-data-json-output',
73+
style=styles['json-output']
74+
)
75+
])
76+
]),
77+
78+
dcc.Tab(label='Selected Data', children=[
79+
html.Div(style=styles['tab'], children=[
80+
html.P('Node Data JSON:'),
81+
html.Pre(
82+
id='selected-node-data-json-output',
83+
style=styles['json-output']
84+
),
85+
html.P('Edge Data JSON:'),
86+
html.Pre(
87+
id='selected-edge-data-json-output',
88+
style=styles['json-output']
89+
)
90+
])
91+
])
92+
]),
93+
94+
])
95+
])
96+
97+
98+
@app.callback(Output('cytoscape', 'elements'),
99+
[Input('remove-button', 'n_clicks')],
100+
[State('cytoscape', 'elements'),
101+
State('cytoscape', 'selectedNodeData')])
102+
def remove_selected_nodes(_, elements, data):
103+
if elements and data:
104+
ids_to_remove = {ele_data['id'] for ele_data in data}
105+
print("Before:", elements)
106+
new_elements = [ele for ele in elements if ele['data']['id'] not in ids_to_remove]
107+
print("After:", new_elements)
108+
return new_elements
109+
110+
return elements
111+
112+
113+
@app.callback(Output('tap-node-data-json-output', 'children'),
114+
[Input('cytoscape', 'tapNodeData')])
115+
def displayTapNodeData(data):
116+
return json.dumps(data, indent=2)
117+
118+
119+
@app.callback(Output('tap-edge-data-json-output', 'children'),
120+
[Input('cytoscape', 'tapEdgeData')])
121+
def displayTapEdgeData(data):
122+
return json.dumps(data, indent=2)
123+
124+
125+
@app.callback(Output('selected-node-data-json-output', 'children'),
126+
[Input('cytoscape', 'selectedNodeData')])
127+
def displaySelectedNodeData(data):
128+
return json.dumps(data, indent=2)
129+
130+
131+
@app.callback(Output('selected-edge-data-json-output', 'children'),
132+
[Input('cytoscape', 'selectedEdgeData')])
133+
def displaySelectedEdgeData(data):
134+
return json.dumps(data, indent=2)
135+
136+
137+
if __name__ == '__main__':
138+
app.run_server(debug=True)

src/lib/components/Cytoscape.react.js

Lines changed: 52 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,56 @@ class Cytoscape extends Component {
158158
window.cy = cy;
159159
this._handleCyCalled = true;
160160

161+
// ///////////////////////////////////// CONSTANTS /////////////////////////////////////////
162+
const SELECT_THRESHOLD = 100;
163+
164+
const selectedNodes = cy.collection();
165+
const selectedEdges = cy.collection();
166+
167+
// ///////////////////////////////////// FUNCTIONS /////////////////////////////////////////
168+
const refreshLayout = _.debounce(() => {
169+
/**
170+
* Refresh Layout if needed
171+
*/
172+
const {
173+
autoRefreshLayout,
174+
layout
175+
} = this.props;
176+
177+
if (autoRefreshLayout) {
178+
cy.layout(layout).run()
179+
}
180+
}, SELECT_THRESHOLD);
181+
182+
const sendSelectedNodesData = _.debounce(() => {
183+
/**
184+
This function is repetitively called every time a node is selected
185+
or unselected, but keeps being debounced if it is called again
186+
within 100 ms (given by SELECT_THRESHOLD). Effectively, it only
187+
runs when all the nodes have been correctly selected/unselected and
188+
added/removed from the selectedNodes collection, and then updates
189+
the selectedNodeData prop.
190+
*/
191+
const nodeData = selectedNodes.map(el => el.data());
192+
193+
if (typeof this.props.setProps === 'function') {
194+
this.props.setProps({
195+
selectedNodeData: nodeData
196+
})
197+
}
198+
}, SELECT_THRESHOLD);
199+
200+
const sendSelectedEdgesData = _.debounce(() => {
201+
const edgeData = selectedEdges.map(el => el.data());
202+
203+
if (typeof this.props.setProps === 'function') {
204+
this.props.setProps({
205+
selectedEdgeData: edgeData
206+
})
207+
}
208+
}, SELECT_THRESHOLD);
209+
210+
// /////////////////////////////////////// EVENTS //////////////////////////////////////////
161211
cy.on('tap', 'node', event => {
162212
const nodeObject = this.generateNode(event);
163213

@@ -196,48 +246,14 @@ class Cytoscape extends Component {
196246
}
197247
});
198248

199-
// SELECTED DATA
200-
const SELECT_THRESHOLD = 100;
201-
202-
const selectedNodes = cy.collection();
203-
const selectedEdges = cy.collection();
204-
205-
const sendSelectedNodesData = _.debounce(() => {
206-
/*
207-
This function is repetitively called every time a node is selected
208-
or unselected, but keeps being debounced if it is called again
209-
within 100 ms (given by SELECT_THRESHOLD). Effectively, it only
210-
runs when all the nodes have been correctly selected/unselected and
211-
added/removed from the selectedNodes collection, and then updates
212-
the selectedNodeData prop.
213-
*/
214-
const nodeData = selectedNodes.map(el => el.data());
215-
216-
if (typeof this.props.setProps === 'function') {
217-
this.props.setProps({
218-
selectedNodeData: nodeData
219-
})
220-
}
221-
}, SELECT_THRESHOLD);
222-
223-
const sendSelectedEdgesData = _.debounce(() => {
224-
const edgeData = selectedEdges.map(el => el.data());
225-
226-
if (typeof this.props.setProps === 'function') {
227-
this.props.setProps({
228-
selectedEdgeData: edgeData
229-
})
230-
}
231-
}, SELECT_THRESHOLD);
232-
233249
cy.on('select', 'node', event => {
234250
const ele = event.target;
235251

236252
selectedNodes.merge(ele);
237253
sendSelectedNodesData();
238254
});
239255

240-
cy.on('unselect', 'node', event => {
256+
cy.on('unselect remove', 'node', event => {
241257
const ele = event.target;
242258

243259
selectedNodes.unmerge(ele);
@@ -251,26 +267,13 @@ class Cytoscape extends Component {
251267
sendSelectedEdgesData();
252268
});
253269

254-
cy.on('unselect', 'edge', event => {
270+
cy.on('unselect remove', 'edge', event => {
255271
const ele = event.target;
256272

257273
selectedEdges.unmerge(ele);
258274
sendSelectedEdgesData();
259275
});
260276

261-
262-
// Refresh Layout if needed
263-
const refreshLayout = _.debounce(() => {
264-
const {
265-
autoRefreshLayout,
266-
layout
267-
} = this.props;
268-
269-
if (autoRefreshLayout) {
270-
cy.layout(layout).run()
271-
}
272-
}, SELECT_THRESHOLD);
273-
274277
cy.on('add remove', () => {
275278
refreshLayout();
276279
});

0 commit comments

Comments
 (0)