Skip to content

Commit 4cba5bc

Browse files
author
Manfred Cheung
authored
Responsive graph merge (#92)
* Add prop to enable responsive resize of the cytoscape graph * Add compensation to handle changing unconstrained dimension during constrained zoom * Improve function and variable names * Reset viewport dimensions on toggle on * Add responsive graph demo * Add responsive graph demo to tests * Pull data for responsive graph demo from cytoscapejs rather than local * Lint responsive graph code * Updated CHANGELOG.md * Update README for responsive feature * Moved cytoscape responsive file to avoid docgen error * Fix formatting errors * Build * Reacquire initial positions and add y offset * Remove package-lock.json
1 parent 4511b89 commit 4cba5bc

14 files changed

+2009
-3465
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
* Added ability to download image files generated with `generateImage` client-side without sending
1414
data to the server.
1515
* Used the newly added `generateImage` and `imageData` properties to enable svg generation using https://github.com/kinimesi/cytoscape-svg
16+
* Added responsive cytoscape.js graph feature toggled using the `responsive` property.
17+
* One new demo:
18+
* `demos/usage-responsive-graph.py`: Example of graph with the ability to toggle the responsive feature on and off.
1619

1720
### Changed
1821
* `utils.Tree`: v0.1.1 broke compatibility with Python 2. Therefore, modified code to be compatible

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,4 +174,8 @@ The Pull Request and Issue Templates were inspired from the
174174
[Code](demos/usage-image-export.py)
175175
![View usage-image-export on Github](demos/images/usage-image-export.gif)
176176

177+
### Make graph responsive
178+
[Code](demos/usage-responsive-graph.py)
179+
![View usage-responsive-graph on Github](demos/images/usage-responsive-graph.gif)
180+
177181
For an extended gallery, visit the [demos' readme](demos/README.md).

dash_cytoscape/Cytoscape.py

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ class Cytoscape(Component):
1212
- id (string; optional): The ID used to identify this component in Dash callbacks.
1313
- className (string; optional): Sets the class name of the element (the value of an element's html
1414
class attribute).
15-
- style (dict; default {width: '600px', height: '600px'}): Add inline styles to the root element.
16-
- elements (list of dicts; optional): A list of dictionaries representing the elements of the networks.
15+
- style (dict; optional): Add inline styles to the root element.
16+
- elements (list; optional): A list of dictionaries representing the elements of the networks.
1717
1. Each dictionary describes an element, and specifies its purpose.
1818
- `group` (string): Either 'nodes' or 'edges'. If not given, it's automatically inferred.
1919
- `data` (dictionary): Element specific data.
@@ -32,7 +32,7 @@ class attribute).
3232
- `classes` (string): Space separated string of class names of the element. Those classes can be selected by a style selector.
3333
3434
2. The [official Cytoscape.js documentation](http://js.cytoscape.org/#notation/elements-json) offers an extensive overview and examples of element declaration.
35-
- stylesheet (list of dicts; optional): A list of dictionaries representing the styles of the elements.
35+
- stylesheet (list; optional): A list of dictionaries representing the styles of the elements.
3636
1. Each dictionary requires the following keys:
3737
- `selector` (string): Which elements you are styling. Generally, you select a group of elements (node, edges, both), a class (that you declare in the element dictionary), or an element by ID.
3838
- `style` (dictionary): What aspects of the elements you want to modify. This could be the size or color of a node, the shape of an edge arrow, or many more.
@@ -42,7 +42,7 @@ class attribute).
4242
exhaustively documented in the Cytoscape.js docs. Although methods such
4343
as `cy.elements(...)` and `cy.filter(...)` are not available, the selector
4444
string syntax stays the same.
45-
- layout (dict; default {name: 'grid'}): A dictionary specifying how to set the position of the elements in your
45+
- layout (dict; optional): A dictionary specifying how to set the position of the elements in your
4646
graph. The `'name'` key is required, and indicates which layout (algorithm) to
4747
use.
4848
1. The layouts available by default are:
@@ -80,33 +80,33 @@ class attribute).
8080
Note that certain keys are not supported in Dash since the value is a
8181
JavaScript function or a callback. Please visit [this issue](https://github.com/plotly/dash-cytoscape/issues/25)
8282
for more information.
83-
- pan (dict; default {x: 0, y: 0}): Dictionary indicating the initial panning position of the graph. The
83+
- pan (dict; optional): Dictionary indicating the initial panning position of the graph. The
8484
following keys are accepted:
8585
- `x` (number): The x-coordinate of the position.
8686
- `y` (number): The y-coordinate of the position.
87-
- zoom (number; default 1): The initial zoom level of the graph. You can set `minZoom` and
87+
- zoom (number; optional): The initial zoom level of the graph. You can set `minZoom` and
8888
`maxZoom` to set restrictions on the zoom level.
89-
- panningEnabled (boolean; default True): Whether panning the graph is enabled (i.e., the position of the graph is
89+
- panningEnabled (boolean; optional): Whether panning the graph is enabled (i.e., the position of the graph is
9090
mutable overall).
91-
- userPanningEnabled (boolean; default True): Whether user events (e.g. dragging the graph background) are allowed to
91+
- userPanningEnabled (boolean; optional): Whether user events (e.g. dragging the graph background) are allowed to
9292
pan the graph.
93-
- minZoom (number; default 1e-50): A minimum bound on the zoom level of the graph. The viewport can not be
93+
- minZoom (number; optional): A minimum bound on the zoom level of the graph. The viewport can not be
9494
scaled smaller than this zoom level.
95-
- maxZoom (number; default 1e50): A maximum bound on the zoom level of the graph. The viewport can not be
95+
- maxZoom (number; optional): A maximum bound on the zoom level of the graph. The viewport can not be
9696
scaled larger than this zoom level.
97-
- zoomingEnabled (boolean; default True): Whether zooming the graph is enabled (i.e., the zoom level of the graph
97+
- zoomingEnabled (boolean; optional): Whether zooming the graph is enabled (i.e., the zoom level of the graph
9898
is mutable overall).
99-
- userZoomingEnabled (boolean; default True): Whether user events (e.g. dragging the graph background) are allowed
99+
- userZoomingEnabled (boolean; optional): Whether user events (e.g. dragging the graph background) are allowed
100100
to pan the graph.
101-
- boxSelectionEnabled (boolean; default False): Whether box selection (i.e. drag a box overlay around, and release it
101+
- boxSelectionEnabled (boolean; optional): Whether box selection (i.e. drag a box overlay around, and release it
102102
to select) is enabled. If enabled, the user must taphold to pan the graph.
103-
- autoungrabify (boolean; default False): Whether nodes should be ungrabified (not grabbable by user) by
103+
- autoungrabify (boolean; optional): Whether nodes should be ungrabified (not grabbable by user) by
104104
default (if true, overrides individual node state).
105-
- autolock (boolean; default False): Whether nodes should be locked (not draggable at all) by default
105+
- autolock (boolean; optional): Whether nodes should be locked (not draggable at all) by default
106106
(if true, overrides individual node state).
107-
- autounselectify (boolean; default False): Whether nodes should be unselectified (immutable selection state) by
107+
- autounselectify (boolean; optional): Whether nodes should be unselectified (immutable selection state) by
108108
default (if true, overrides individual element state).
109-
- autoRefreshLayout (boolean; default True): Whether the layout should be refreshed when elements are added or removed.
109+
- autoRefreshLayout (boolean; optional): Whether the layout should be refreshed when elements are added or removed.
110110
- tapNode (dict; optional): The complete node dictionary returned when you tap or click it. Read-only.
111111
112112
1. Node-specific items:
@@ -187,14 +187,15 @@ class attribute).
187187
the image, it may be prudent to invoke `'download'` for `action` instead of
188188
`'store'` to improve performance by preventing transfer of data to the server.
189189
- imageData (string; optional): String representation of the image requested with generateImage. Null if no
190-
image was requested yet or the previous request failed. Read-only."""
190+
image was requested yet or the previous request failed. Read-only.
191+
- responsive (boolean; optional): Toggles intelligent responsive resize of Cytoscape graph with viewport size change"""
191192
@_explicitize_args
192-
def __init__(self, id=Component.UNDEFINED, className=Component.UNDEFINED, style=Component.UNDEFINED, elements=Component.UNDEFINED, stylesheet=Component.UNDEFINED, layout=Component.UNDEFINED, pan=Component.UNDEFINED, zoom=Component.UNDEFINED, panningEnabled=Component.UNDEFINED, userPanningEnabled=Component.UNDEFINED, minZoom=Component.UNDEFINED, maxZoom=Component.UNDEFINED, zoomingEnabled=Component.UNDEFINED, userZoomingEnabled=Component.UNDEFINED, boxSelectionEnabled=Component.UNDEFINED, autoungrabify=Component.UNDEFINED, autolock=Component.UNDEFINED, autounselectify=Component.UNDEFINED, autoRefreshLayout=Component.UNDEFINED, tapNode=Component.UNDEFINED, tapNodeData=Component.UNDEFINED, tapEdge=Component.UNDEFINED, tapEdgeData=Component.UNDEFINED, mouseoverNodeData=Component.UNDEFINED, mouseoverEdgeData=Component.UNDEFINED, selectedNodeData=Component.UNDEFINED, selectedEdgeData=Component.UNDEFINED, generateImage=Component.UNDEFINED, imageData=Component.UNDEFINED, **kwargs):
193-
self._prop_names = ['id', 'className', 'style', 'elements', 'stylesheet', 'layout', 'pan', 'zoom', 'panningEnabled', 'userPanningEnabled', 'minZoom', 'maxZoom', 'zoomingEnabled', 'userZoomingEnabled', 'boxSelectionEnabled', 'autoungrabify', 'autolock', 'autounselectify', 'autoRefreshLayout', 'tapNode', 'tapNodeData', 'tapEdge', 'tapEdgeData', 'mouseoverNodeData', 'mouseoverEdgeData', 'selectedNodeData', 'selectedEdgeData', 'generateImage', 'imageData']
193+
def __init__(self, id=Component.UNDEFINED, className=Component.UNDEFINED, style=Component.UNDEFINED, elements=Component.UNDEFINED, stylesheet=Component.UNDEFINED, layout=Component.UNDEFINED, pan=Component.UNDEFINED, zoom=Component.UNDEFINED, panningEnabled=Component.UNDEFINED, userPanningEnabled=Component.UNDEFINED, minZoom=Component.UNDEFINED, maxZoom=Component.UNDEFINED, zoomingEnabled=Component.UNDEFINED, userZoomingEnabled=Component.UNDEFINED, boxSelectionEnabled=Component.UNDEFINED, autoungrabify=Component.UNDEFINED, autolock=Component.UNDEFINED, autounselectify=Component.UNDEFINED, autoRefreshLayout=Component.UNDEFINED, tapNode=Component.UNDEFINED, tapNodeData=Component.UNDEFINED, tapEdge=Component.UNDEFINED, tapEdgeData=Component.UNDEFINED, mouseoverNodeData=Component.UNDEFINED, mouseoverEdgeData=Component.UNDEFINED, selectedNodeData=Component.UNDEFINED, selectedEdgeData=Component.UNDEFINED, generateImage=Component.UNDEFINED, imageData=Component.UNDEFINED, responsive=Component.UNDEFINED, **kwargs):
194+
self._prop_names = ['id', 'className', 'style', 'elements', 'stylesheet', 'layout', 'pan', 'zoom', 'panningEnabled', 'userPanningEnabled', 'minZoom', 'maxZoom', 'zoomingEnabled', 'userZoomingEnabled', 'boxSelectionEnabled', 'autoungrabify', 'autolock', 'autounselectify', 'autoRefreshLayout', 'tapNode', 'tapNodeData', 'tapEdge', 'tapEdgeData', 'mouseoverNodeData', 'mouseoverEdgeData', 'selectedNodeData', 'selectedEdgeData', 'generateImage', 'imageData', 'responsive']
194195
self._type = 'Cytoscape'
195196
self._namespace = 'dash_cytoscape'
196197
self._valid_wildcard_attributes = []
197-
self.available_properties = ['id', 'className', 'style', 'elements', 'stylesheet', 'layout', 'pan', 'zoom', 'panningEnabled', 'userPanningEnabled', 'minZoom', 'maxZoom', 'zoomingEnabled', 'userZoomingEnabled', 'boxSelectionEnabled', 'autoungrabify', 'autolock', 'autounselectify', 'autoRefreshLayout', 'tapNode', 'tapNodeData', 'tapEdge', 'tapEdgeData', 'mouseoverNodeData', 'mouseoverEdgeData', 'selectedNodeData', 'selectedEdgeData', 'generateImage', 'imageData']
198+
self.available_properties = ['id', 'className', 'style', 'elements', 'stylesheet', 'layout', 'pan', 'zoom', 'panningEnabled', 'userPanningEnabled', 'minZoom', 'maxZoom', 'zoomingEnabled', 'userZoomingEnabled', 'boxSelectionEnabled', 'autoungrabify', 'autolock', 'autounselectify', 'autoRefreshLayout', 'tapNode', 'tapNodeData', 'tapEdge', 'tapEdgeData', 'mouseoverNodeData', 'mouseoverEdgeData', 'selectedNodeData', 'selectedEdgeData', 'generateImage', 'imageData', 'responsive']
198199
self.available_wildcard_properties = []
199200

200201
_explicit_args = kwargs.pop('_explicit_args')

dash_cytoscape/dash_cytoscape.dev.js

Lines changed: 43 additions & 6 deletions
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: 8 additions & 13 deletions
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: 1521 additions & 3398 deletions
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: 12 additions & 17 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dash_cytoscape/metadata.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,17 @@
364364
"value": "null",
365365
"computed": false
366366
}
367+
},
368+
"responsive": {
369+
"type": {
370+
"name": "bool"
371+
},
372+
"required": false,
373+
"description": "Toggles intelligent responsive resize of Cytoscape graph with viewport size change",
374+
"defaultValue": {
375+
"value": "false",
376+
"computed": false
377+
}
367378
}
368379
}
369380
}
807 KB
Loading

demos/usage-responsive-graph.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
"""
2+
Original Demo: http://js.cytoscape.org/demos/cose-layout/
3+
4+
Note: This implementation looks different from the original implementation,
5+
although the input paramters are exactly the same.
6+
"""
7+
import urllib.request
8+
import json
9+
10+
import dash
11+
from dash.dependencies import Input, Output
12+
import dash_html_components as html
13+
14+
import dash_cytoscape as cyto
15+
16+
app = dash.Dash(__name__)
17+
server = app.server
18+
19+
app.scripts.config.serve_locally = True
20+
app.css.config.serve_locally = True
21+
22+
# Load Data
23+
with urllib.request.urlopen('https://js.cytoscape.org/demos/colajs-graph/data.json') as url:
24+
elements = json.loads(url.read().decode())
25+
26+
with urllib.request.urlopen('https://js.cytoscape.org/demos/colajs-graph/cy-style.json') as url:
27+
stylesheet = json.loads(url.read().decode())
28+
29+
styles = {
30+
'container': {
31+
'position': 'fixed',
32+
'display': 'flex',
33+
'flex-direction': 'column',
34+
'height': '100%',
35+
'width': '100%'
36+
},
37+
'cy-container': {
38+
'flex': '1',
39+
'position': 'relative'
40+
},
41+
'cytoscape': {
42+
'position': 'absolute',
43+
'width': '100%',
44+
'height': '100%',
45+
'z-index': 999
46+
}
47+
}
48+
49+
# App
50+
app.layout = html.Div(style=styles['container'], children=[
51+
html.Div([
52+
html.Button("Responsive Toggle", id='toggle-button'),
53+
html.Div(id='toggle-text')
54+
]),
55+
html.Div(className='cy-container', style=styles['cy-container'], children=[
56+
cyto.Cytoscape(
57+
id='cytoscape',
58+
elements=elements,
59+
stylesheet=stylesheet,
60+
style=styles['cytoscape'],
61+
layout={
62+
'name': 'cose',
63+
'idealEdgeLength': 100,
64+
'nodeOverlap': 20,
65+
'refresh': 20,
66+
'fit': True,
67+
'padding': 30,
68+
'randomize': False,
69+
'componentSpacing': 100,
70+
'nodeRepulsion': 400000,
71+
'edgeElasticity': 100,
72+
'nestingFactor': 5,
73+
'gravity': 80,
74+
'numIter': 1000,
75+
'initialTemp': 200,
76+
'coolingFactor': 0.95,
77+
'minTemp': 1.0
78+
},
79+
responsive=True
80+
)
81+
])
82+
])
83+
84+
85+
@app.callback(Output('cytoscape', 'responsive'), [Input('toggle-button', 'n_clicks')])
86+
def toggle_responsive(n_clicks):
87+
n_clicks = 2 if n_clicks is None else n_clicks
88+
toggle_on = n_clicks % 2 == 0
89+
return toggle_on
90+
91+
92+
@app.callback(Output('toggle-text', 'children'), [Input('cytoscape', 'responsive')])
93+
def update_toggle_text(responsive):
94+
return '\t' + 'Responsive ' + ('On' if responsive else 'Off')
95+
96+
97+
if __name__ == '__main__':
98+
app.run_server(debug=True)

0 commit comments

Comments
 (0)