Skip to content

Commit cdfea33

Browse files
authored
Merge pull request #200 from plotly/leaflet-aio
Add CyLeaflet AIO component
2 parents b070ded + 0620d28 commit cdfea33

31 files changed

+727
-21
lines changed

R/cytoCytoscape.R

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
# AUTO GENERATED FILE - DO NOT EDIT
22

33
#' @export
4-
cytoCytoscape <- function(id=NULL, autoRefreshLayout=NULL, autolock=NULL, autoungrabify=NULL, autounselectify=NULL, boxSelectionEnabled=NULL, className=NULL, clearOnUnhover=NULL, contextMenu=NULL, contextMenuData=NULL, elements=NULL, generateImage=NULL, imageData=NULL, layout=NULL, maxZoom=NULL, minZoom=NULL, mouseoverEdgeData=NULL, mouseoverNodeData=NULL, pan=NULL, panningEnabled=NULL, responsive=NULL, selectedEdgeData=NULL, selectedNodeData=NULL, style=NULL, stylesheet=NULL, tapEdge=NULL, tapEdgeData=NULL, tapNode=NULL, tapNodeData=NULL, userPanningEnabled=NULL, userZoomingEnabled=NULL, zoom=NULL, zoomingEnabled=NULL) {
4+
cytoCytoscape <- function(id=NULL, autoRefreshLayout=NULL, autolock=NULL, autoungrabify=NULL, autounselectify=NULL, boxSelectionEnabled=NULL, className=NULL, clearOnUnhover=NULL, contextMenu=NULL, contextMenuData=NULL, elements=NULL, extent=NULL, generateImage=NULL, imageData=NULL, layout=NULL, maxZoom=NULL, minZoom=NULL, mouseoverEdgeData=NULL, mouseoverNodeData=NULL, pan=NULL, panningEnabled=NULL, responsive=NULL, selectedEdgeData=NULL, selectedNodeData=NULL, style=NULL, stylesheet=NULL, tapEdge=NULL, tapEdgeData=NULL, tapNode=NULL, tapNodeData=NULL, userPanningEnabled=NULL, userZoomingEnabled=NULL, zoom=NULL, zoomingEnabled=NULL) {
55

6-
props <- list(id=id, autoRefreshLayout=autoRefreshLayout, autolock=autolock, autoungrabify=autoungrabify, autounselectify=autounselectify, boxSelectionEnabled=boxSelectionEnabled, className=className, clearOnUnhover=clearOnUnhover, contextMenu=contextMenu, contextMenuData=contextMenuData, elements=elements, generateImage=generateImage, imageData=imageData, layout=layout, maxZoom=maxZoom, minZoom=minZoom, mouseoverEdgeData=mouseoverEdgeData, mouseoverNodeData=mouseoverNodeData, pan=pan, panningEnabled=panningEnabled, responsive=responsive, selectedEdgeData=selectedEdgeData, selectedNodeData=selectedNodeData, style=style, stylesheet=stylesheet, tapEdge=tapEdge, tapEdgeData=tapEdgeData, tapNode=tapNode, tapNodeData=tapNodeData, userPanningEnabled=userPanningEnabled, userZoomingEnabled=userZoomingEnabled, zoom=zoom, zoomingEnabled=zoomingEnabled)
6+
props <- list(id=id, autoRefreshLayout=autoRefreshLayout, autolock=autolock, autoungrabify=autoungrabify, autounselectify=autounselectify, boxSelectionEnabled=boxSelectionEnabled, className=className, clearOnUnhover=clearOnUnhover, contextMenu=contextMenu, contextMenuData=contextMenuData, elements=elements, extent=extent, generateImage=generateImage, imageData=imageData, layout=layout, maxZoom=maxZoom, minZoom=minZoom, mouseoverEdgeData=mouseoverEdgeData, mouseoverNodeData=mouseoverNodeData, pan=pan, panningEnabled=panningEnabled, responsive=responsive, selectedEdgeData=selectedEdgeData, selectedNodeData=selectedNodeData, style=style, stylesheet=stylesheet, tapEdge=tapEdge, tapEdgeData=tapEdgeData, tapNode=tapNode, tapNodeData=tapNodeData, userPanningEnabled=userPanningEnabled, userZoomingEnabled=userZoomingEnabled, zoom=zoom, zoomingEnabled=zoomingEnabled)
77
if (length(props) > 0) {
88
props <- props[!vapply(props, is.null, logical(1))]
99
}
1010
component <- list(
1111
props = props,
1212
type = 'Cytoscape',
1313
namespace = 'dash_cytoscape',
14-
propNames = c('id', 'autoRefreshLayout', 'autolock', 'autoungrabify', 'autounselectify', 'boxSelectionEnabled', 'className', 'clearOnUnhover', 'contextMenu', 'contextMenuData', 'elements', 'generateImage', 'imageData', 'layout', 'maxZoom', 'minZoom', 'mouseoverEdgeData', 'mouseoverNodeData', 'pan', 'panningEnabled', 'responsive', 'selectedEdgeData', 'selectedNodeData', 'style', 'stylesheet', 'tapEdge', 'tapEdgeData', 'tapNode', 'tapNodeData', 'userPanningEnabled', 'userZoomingEnabled', 'zoom', 'zoomingEnabled'),
14+
propNames = c('id', 'autoRefreshLayout', 'autolock', 'autoungrabify', 'autounselectify', 'boxSelectionEnabled', 'className', 'clearOnUnhover', 'contextMenu', 'contextMenuData', 'elements', 'extent', 'generateImage', 'imageData', 'layout', 'maxZoom', 'minZoom', 'mouseoverEdgeData', 'mouseoverNodeData', 'pan', 'panningEnabled', 'responsive', 'selectedEdgeData', 'selectedNodeData', 'style', 'stylesheet', 'tapEdge', 'tapEdgeData', 'tapNode', 'tapNodeData', 'userPanningEnabled', 'userZoomingEnabled', 'zoom', 'zoomingEnabled'),
1515
package = 'dashCytoscape'
1616
)
1717

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ Install the library using `pip`:
3333
pip install dash-cytoscape
3434
```
3535

36+
If you wish to use the CyLeaflet mapping extension, you must install the optional `leaflet` dependencies:
37+
38+
```
39+
pip install dash-cytoscape[leaflet]
40+
```
41+
3642
Create the following example inside an `app.py` file:
3743

3844
```python

dash_cytoscape/CyLeaflet.py

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
from dash import (
2+
clientside_callback,
3+
callback,
4+
ClientsideFunction,
5+
Output,
6+
Input,
7+
html,
8+
dcc,
9+
MATCH,
10+
)
11+
12+
import dash_cytoscape as cyto
13+
14+
try:
15+
import dash_leaflet as dl
16+
except ImportError:
17+
dl = None
18+
19+
# Max zoom of default Leaflet tile layer
20+
LEAFLET_DEFAULT_MAX_ZOOM = 18
21+
22+
# Empirically-determined max zoom values for Cytoscape
23+
# which correspond to max zoom values of Leaflet
24+
LEAF_TO_CYTO_MAX_ZOOM_MAPPING = {
25+
16: 0.418,
26+
17: 0.837,
27+
18: 1.674,
28+
19: 3.349,
29+
20: 6.698,
30+
21: 13.396,
31+
22: 26.793,
32+
}
33+
34+
35+
class CyLeaflet(html.Div):
36+
# Predefined Leaflet tile layer with max zoom of 19
37+
OSM = (
38+
dl.TileLayer(
39+
url="https://tile.openstreetmap.org/{z}/{x}/{y}.png",
40+
maxZoom=19,
41+
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
42+
)
43+
if dl
44+
else None
45+
)
46+
47+
def __init__(
48+
self,
49+
id,
50+
cytoscape_props=None,
51+
leaflet_props=None,
52+
width="600px",
53+
height="480px",
54+
tiles=None,
55+
):
56+
# Throw error if `dash_leaflet` package is not installed
57+
if dl is None:
58+
raise ImportError(
59+
"dash_leaflet not found. Please install it, either directly (`pip install dash_leaflet`) "
60+
+ "or by using `pip install dash_cytoscape[leaflet]`"
61+
)
62+
63+
self.ids = {
64+
s: {"id": id, "component": "cyleaflet", "sub": s}
65+
for s in ["cy", "leaf", "elements"]
66+
}
67+
self.ids["component"] = "leaflet"
68+
cytoscape_props = cytoscape_props or {}
69+
leaflet_props = leaflet_props or {}
70+
elements = cytoscape_props.get("elements", [])
71+
cytoscape_props, leaflet_props = self.set_default_props_and_overrides(
72+
cytoscape_props, leaflet_props, tiles
73+
)
74+
75+
super().__init__(
76+
html.Div(
77+
[
78+
html.Div(
79+
cyto.Cytoscape(**cytoscape_props),
80+
style={
81+
"height": "100%",
82+
"width": "100%",
83+
"position": "absolute",
84+
"top": 0,
85+
"left": 0,
86+
"zIndex": 2,
87+
},
88+
),
89+
html.Div(
90+
dl.Map(**leaflet_props),
91+
style={
92+
"height": "100%",
93+
"width": "100%",
94+
"position": "absolute",
95+
"top": 0,
96+
"left": 0,
97+
"zIndex": 1,
98+
},
99+
),
100+
dcc.Store(id=self.ids["elements"], data=elements),
101+
],
102+
style={
103+
"width": width,
104+
"height": height,
105+
},
106+
),
107+
style={
108+
"height": "100%",
109+
"width": "100%",
110+
"position": "relative",
111+
},
112+
)
113+
114+
def set_default_props_and_overrides(
115+
self, user_cytoscape_props, user_leaflet_props, tiles
116+
):
117+
# If `tiles` is specified, append to end of Leaflet children
118+
# This will make it the visible TileLayer
119+
leaflet_children = user_leaflet_props.get("children", [])
120+
if not isinstance(leaflet_children, list):
121+
leaflet_children = [leaflet_children]
122+
if tiles is not None:
123+
leaflet_children.append(tiles)
124+
125+
# Try to figure out Leaflet maxZoom from Leaflet children,
126+
# then convert to Cytoscape max zoom
127+
leaflet_max_zoom = self.get_leaflet_max_zoom(leaflet_children)
128+
cytoscape_max_zoom = self.get_cytoscape_max_zoom(leaflet_max_zoom)
129+
130+
# Props where we want to override values supplied by the user
131+
# These are props which are required for CyLeaflet to work properly
132+
cytoscape_overrides = {
133+
"id": self.ids["cy"],
134+
"elements": [], # Elements are set via clientside callback, so set to empty list initially
135+
"layout": {"name": "preset", "fit": False},
136+
"style": {"width": "100%", "height": "100%"},
137+
"minZoom": 3 / 100000,
138+
}
139+
# Note: Leaflet MUST be initialized with a center and zoom to avoid an error,
140+
# even though these will be immediately overwritten by syncing w/ Cytoscape
141+
leaflet_overrides = {
142+
"id": self.ids["leaf"],
143+
"children": leaflet_children or [dl.TileLayer()],
144+
"center": [0, 0],
145+
"zoom": 6,
146+
"zoomSnap": 0,
147+
"zoomControl": False,
148+
"zoomAnimation": False,
149+
"maxZoom": 100000,
150+
"maxBoundsViscosity": 1,
151+
"maxBounds": [[-85, -180.0], [85, 180.0]],
152+
"style": {"width": "100%", "height": "100%"},
153+
}
154+
155+
# Props where we want to fill in a default value
156+
# if a value is not supplied by the user
157+
cytoscape_defaults = {
158+
"boxSelectionEnabled": True,
159+
"maxZoom": cytoscape_max_zoom,
160+
}
161+
leaflet_defaults = {}
162+
163+
# Start with default props
164+
cytoscape_props = dict(cytoscape_defaults)
165+
leaflet_props = dict(leaflet_defaults)
166+
167+
# Update with user-supplied props
168+
cytoscape_props.update(user_cytoscape_props)
169+
leaflet_props.update(user_leaflet_props)
170+
171+
# Update with overrides
172+
cytoscape_props.update(cytoscape_overrides)
173+
leaflet_props.update(leaflet_overrides)
174+
175+
return cytoscape_props, leaflet_props
176+
177+
# Try to figure out Leaflet maxZoom from Leaflet children
178+
# If not possible, return the maxZoom of the default Leaflet tile layer
179+
def get_leaflet_max_zoom(self, leaflet_children):
180+
if leaflet_children is None or leaflet_children == []:
181+
return LEAFLET_DEFAULT_MAX_ZOOM
182+
183+
max_zooms = [
184+
c.maxZoom
185+
for c in leaflet_children
186+
if isinstance(c, dl.TileLayer) and hasattr(c, "maxZoom")
187+
]
188+
189+
return max_zooms[-1] if len(max_zooms) > 0 else LEAFLET_DEFAULT_MAX_ZOOM
190+
191+
# Given a maxZoom value for Leaflet, map it to the corresponding maxZoom value for Cytoscape
192+
# If the value is out of range, return the closest value
193+
def get_cytoscape_max_zoom(self, leaflet_max_zoom):
194+
leaflet_max_zoom = leaflet_max_zoom or 0
195+
leaflet_max_zoom = min(
196+
leaflet_max_zoom, max(LEAF_TO_CYTO_MAX_ZOOM_MAPPING.keys())
197+
)
198+
leaflet_max_zoom = max(
199+
leaflet_max_zoom, min(LEAF_TO_CYTO_MAX_ZOOM_MAPPING.keys())
200+
)
201+
return LEAF_TO_CYTO_MAX_ZOOM_MAPPING[leaflet_max_zoom]
202+
203+
204+
if dl is not None:
205+
clientside_callback(
206+
ClientsideFunction(namespace="cyleaflet", function_name="updateLeafBounds"),
207+
Output(
208+
{"id": MATCH, "component": "cyleaflet", "sub": "leaf"}, "invalidateSize"
209+
),
210+
Output({"id": MATCH, "component": "cyleaflet", "sub": "leaf"}, "viewport"),
211+
Input({"id": MATCH, "component": "cyleaflet", "sub": "cy"}, "extent"),
212+
)
213+
clientside_callback(
214+
ClientsideFunction(namespace="cyleaflet", function_name="transformElements"),
215+
Output({"id": MATCH, "component": "cyleaflet", "sub": "cy"}, "elements"),
216+
Input({"id": MATCH, "component": "cyleaflet", "sub": "elements"}, "data"),
217+
)

dash_cytoscape/Cytoscape.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,12 @@ class attribute).
185185
186186
- nodes (list; optional)
187187
188+
- extent (dict; optional):
189+
Extent of the viewport, a bounding box in model co-ordinates that
190+
lets you know what model positions are visible in the viewport.
191+
This function returns a plain object bounding box with format {
192+
x1, y1, x2, y2, w, h }.
193+
188194
- generateImage (dict; optional):
189195
Dictionary specifying options to generate an image of the current
190196
cytoscape graph. Value is cleared after data is received and image
@@ -540,6 +546,7 @@ def __init__(
540546
generateImage=Component.UNDEFINED,
541547
imageData=Component.UNDEFINED,
542548
responsive=Component.UNDEFINED,
549+
extent=Component.UNDEFINED,
543550
clearOnUnhover=Component.UNDEFINED,
544551
**kwargs
545552
):
@@ -555,6 +562,7 @@ def __init__(
555562
"contextMenu",
556563
"contextMenuData",
557564
"elements",
565+
"extent",
558566
"generateImage",
559567
"imageData",
560568
"layout",
@@ -591,6 +599,7 @@ def __init__(
591599
"contextMenu",
592600
"contextMenuData",
593601
"elements",
602+
"extent",
594603
"generateImage",
595604
"imageData",
596605
"layout",

dash_cytoscape/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
from ._imports_ import __all__
1212
from . import utils
1313

14+
# Import CyLeaflet AIO component
15+
from .CyLeaflet import CyLeaflet
16+
1417

1518
if not hasattr(_dash, "__plotly_dash") and not hasattr(_dash, "development"):
1619
print(

dash_cytoscape/dash_cytoscape.dev.js

Lines changed: 21 additions & 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.min.js.LICENSE.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ Copyright (c) 2013-2014 Ralf S. Engelschall (http://engelschall.com)
44
Licensed under The MIT License (http://opensource.org/licenses/MIT)
55
*/
66

7+
/*! (c) Andrea Giammarchi - ISC */
8+
9+
/*! (c) Andrea Giammarchi @webreflection ISC */
10+
711
/*! Bezier curve function generator. Copyright Gaetan Renaudeau. MIT License: http://en.wikipedia.org/wiki/MIT_License */
812

913
/*! Runge-Kutta spring physics function generator. Adapted from Framer.js, copyright Koen Bok. MIT License: http://en.wikipedia.org/wiki/MIT_License */

dash_cytoscape/dash_cytoscape_extra.dev.js

Lines changed: 21 additions & 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.

0 commit comments

Comments
 (0)