Skip to content

Commit

Permalink
Remove selected nodes after they are deleted (#49)
Browse files Browse the repository at this point in the history
* Fix #45

* Fix PEP8
  • Loading branch information
Xing committed Apr 5, 2019
1 parent 0d2b47f commit 8b08621
Show file tree
Hide file tree
Showing 6 changed files with 194 additions and 53 deletions.
2 changes: 1 addition & 1 deletion dash_cytoscape/dash_cytoscape.dev.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dash_cytoscape/dash_cytoscape.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dash_cytoscape/dash_cytoscape_extra.dev.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dash_cytoscape/dash_cytoscape_extra.min.js

Large diffs are not rendered by default.

138 changes: 138 additions & 0 deletions demos/usage-remove-selected-elements.py
@@ -0,0 +1,138 @@
import json
import os
import random

import dash
from dash.dependencies import Input, Output, State
import dash_core_components as dcc
import dash_html_components as html

import dash_cytoscape as cyto

asset_path = os.path.join(
os.path.dirname(os.path.abspath(__file__)),
'..', 'assets'
)

app = dash.Dash(__name__, assets_folder=asset_path)
server = app.server

random.seed(2019)

nodes = [
{'data': {'id': str(i), 'label': 'Node {}'.format(i)}}
for i in range(1, 21)
]

edges = [
{'data': {'source': str(random.randint(1, 20)), 'target': str(random.randint(1, 20))}}
for _ in range(30)
]

default_elements = nodes + edges

styles = {
'json-output': {
'overflow-y': 'scroll',
'height': 'calc(50% - 25px)',
'border': 'thin lightgrey solid'
},
'tab': {'height': 'calc(98vh - 115px)'}
}

app.layout = html.Div([
html.Div(className='eight columns', children=[
cyto.Cytoscape(
id='cytoscape',
elements=default_elements,
layout={
'name': 'grid'
},
style={
'height': '95vh',
'width': '100%'
}
)
]),

html.Div(className='four columns', children=[
dcc.Tabs(id='tabs', children=[
dcc.Tab(label='Actions', children=[
html.Button("Remove Selected Node", id='remove-button')
]),
dcc.Tab(label='Tap Data', children=[
html.Div(style=styles['tab'], children=[
html.P('Node Data JSON:'),
html.Pre(
id='tap-node-data-json-output',
style=styles['json-output']
),
html.P('Edge Data JSON:'),
html.Pre(
id='tap-edge-data-json-output',
style=styles['json-output']
)
])
]),

dcc.Tab(label='Selected Data', children=[
html.Div(style=styles['tab'], children=[
html.P('Node Data JSON:'),
html.Pre(
id='selected-node-data-json-output',
style=styles['json-output']
),
html.P('Edge Data JSON:'),
html.Pre(
id='selected-edge-data-json-output',
style=styles['json-output']
)
])
])
]),

])
])


@app.callback(Output('cytoscape', 'elements'),
[Input('remove-button', 'n_clicks')],
[State('cytoscape', 'elements'),
State('cytoscape', 'selectedNodeData')])
def remove_selected_nodes(_, elements, data):
if elements and data:
ids_to_remove = {ele_data['id'] for ele_data in data}
print("Before:", elements)
new_elements = [ele for ele in elements if ele['data']['id'] not in ids_to_remove]
print("After:", new_elements)
return new_elements

return elements


@app.callback(Output('tap-node-data-json-output', 'children'),
[Input('cytoscape', 'tapNodeData')])
def displayTapNodeData(data):
return json.dumps(data, indent=2)


@app.callback(Output('tap-edge-data-json-output', 'children'),
[Input('cytoscape', 'tapEdgeData')])
def displayTapEdgeData(data):
return json.dumps(data, indent=2)


@app.callback(Output('selected-node-data-json-output', 'children'),
[Input('cytoscape', 'selectedNodeData')])
def displaySelectedNodeData(data):
return json.dumps(data, indent=2)


@app.callback(Output('selected-edge-data-json-output', 'children'),
[Input('cytoscape', 'selectedEdgeData')])
def displaySelectedEdgeData(data):
return json.dumps(data, indent=2)


if __name__ == '__main__':
app.run_server(debug=True)
101 changes: 52 additions & 49 deletions src/lib/components/Cytoscape.react.js
Expand Up @@ -158,6 +158,56 @@ class Cytoscape extends Component {
window.cy = cy;
this._handleCyCalled = true;

// ///////////////////////////////////// CONSTANTS /////////////////////////////////////////
const SELECT_THRESHOLD = 100;

const selectedNodes = cy.collection();
const selectedEdges = cy.collection();

// ///////////////////////////////////// FUNCTIONS /////////////////////////////////////////
const refreshLayout = _.debounce(() => {
/**
* Refresh Layout if needed
*/
const {
autoRefreshLayout,
layout
} = this.props;

if (autoRefreshLayout) {
cy.layout(layout).run()
}
}, SELECT_THRESHOLD);

const sendSelectedNodesData = _.debounce(() => {
/**
This function is repetitively called every time a node is selected
or unselected, but keeps being debounced if it is called again
within 100 ms (given by SELECT_THRESHOLD). Effectively, it only
runs when all the nodes have been correctly selected/unselected and
added/removed from the selectedNodes collection, and then updates
the selectedNodeData prop.
*/
const nodeData = selectedNodes.map(el => el.data());

if (typeof this.props.setProps === 'function') {
this.props.setProps({
selectedNodeData: nodeData
})
}
}, SELECT_THRESHOLD);

const sendSelectedEdgesData = _.debounce(() => {
const edgeData = selectedEdges.map(el => el.data());

if (typeof this.props.setProps === 'function') {
this.props.setProps({
selectedEdgeData: edgeData
})
}
}, SELECT_THRESHOLD);

// /////////////////////////////////////// EVENTS //////////////////////////////////////////
cy.on('tap', 'node', event => {
const nodeObject = this.generateNode(event);

Expand Down Expand Up @@ -196,48 +246,14 @@ class Cytoscape extends Component {
}
});

// SELECTED DATA
const SELECT_THRESHOLD = 100;

const selectedNodes = cy.collection();
const selectedEdges = cy.collection();

const sendSelectedNodesData = _.debounce(() => {
/*
This function is repetitively called every time a node is selected
or unselected, but keeps being debounced if it is called again
within 100 ms (given by SELECT_THRESHOLD). Effectively, it only
runs when all the nodes have been correctly selected/unselected and
added/removed from the selectedNodes collection, and then updates
the selectedNodeData prop.
*/
const nodeData = selectedNodes.map(el => el.data());

if (typeof this.props.setProps === 'function') {
this.props.setProps({
selectedNodeData: nodeData
})
}
}, SELECT_THRESHOLD);

const sendSelectedEdgesData = _.debounce(() => {
const edgeData = selectedEdges.map(el => el.data());

if (typeof this.props.setProps === 'function') {
this.props.setProps({
selectedEdgeData: edgeData
})
}
}, SELECT_THRESHOLD);

cy.on('select', 'node', event => {
const ele = event.target;

selectedNodes.merge(ele);
sendSelectedNodesData();
});

cy.on('unselect', 'node', event => {
cy.on('unselect remove', 'node', event => {
const ele = event.target;

selectedNodes.unmerge(ele);
Expand All @@ -251,26 +267,13 @@ class Cytoscape extends Component {
sendSelectedEdgesData();
});

cy.on('unselect', 'edge', event => {
cy.on('unselect remove', 'edge', event => {
const ele = event.target;

selectedEdges.unmerge(ele);
sendSelectedEdgesData();
});


// Refresh Layout if needed
const refreshLayout = _.debounce(() => {
const {
autoRefreshLayout,
layout
} = this.props;

if (autoRefreshLayout) {
cy.layout(layout).run()
}
}, SELECT_THRESHOLD);

cy.on('add remove', () => {
refreshLayout();
});
Expand Down

0 comments on commit 8b08621

Please sign in to comment.