Skip to content

Commit

Permalink
Merge pull request #200 from plotly/leaflet-aio
Browse files Browse the repository at this point in the history
Add CyLeaflet AIO component
  • Loading branch information
emilykl committed Jan 22, 2024
2 parents b070ded + 0620d28 commit cdfea33
Show file tree
Hide file tree
Showing 31 changed files with 727 additions and 21 deletions.
6 changes: 3 additions & 3 deletions R/cytoCytoscape.R
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
# AUTO GENERATED FILE - DO NOT EDIT

#' @export
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) {
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) {

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)
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)
if (length(props) > 0) {
props <- props[!vapply(props, is.null, logical(1))]
}
component <- list(
props = props,
type = 'Cytoscape',
namespace = 'dash_cytoscape',
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'),
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'),
package = 'dashCytoscape'
)

Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ Install the library using `pip`:
pip install dash-cytoscape
```

If you wish to use the CyLeaflet mapping extension, you must install the optional `leaflet` dependencies:

```
pip install dash-cytoscape[leaflet]
```

Create the following example inside an `app.py` file:

```python
Expand Down
217 changes: 217 additions & 0 deletions dash_cytoscape/CyLeaflet.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
from dash import (
clientside_callback,
callback,
ClientsideFunction,
Output,
Input,
html,
dcc,
MATCH,
)

import dash_cytoscape as cyto

try:
import dash_leaflet as dl
except ImportError:
dl = None

# Max zoom of default Leaflet tile layer
LEAFLET_DEFAULT_MAX_ZOOM = 18

# Empirically-determined max zoom values for Cytoscape
# which correspond to max zoom values of Leaflet
LEAF_TO_CYTO_MAX_ZOOM_MAPPING = {
16: 0.418,
17: 0.837,
18: 1.674,
19: 3.349,
20: 6.698,
21: 13.396,
22: 26.793,
}


class CyLeaflet(html.Div):
# Predefined Leaflet tile layer with max zoom of 19
OSM = (
dl.TileLayer(
url="https://tile.openstreetmap.org/{z}/{x}/{y}.png",
maxZoom=19,
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
)
if dl
else None
)

def __init__(
self,
id,
cytoscape_props=None,
leaflet_props=None,
width="600px",
height="480px",
tiles=None,
):
# Throw error if `dash_leaflet` package is not installed
if dl is None:
raise ImportError(
"dash_leaflet not found. Please install it, either directly (`pip install dash_leaflet`) "
+ "or by using `pip install dash_cytoscape[leaflet]`"
)

self.ids = {
s: {"id": id, "component": "cyleaflet", "sub": s}
for s in ["cy", "leaf", "elements"]
}
self.ids["component"] = "leaflet"
cytoscape_props = cytoscape_props or {}
leaflet_props = leaflet_props or {}
elements = cytoscape_props.get("elements", [])
cytoscape_props, leaflet_props = self.set_default_props_and_overrides(
cytoscape_props, leaflet_props, tiles
)

super().__init__(
html.Div(
[
html.Div(
cyto.Cytoscape(**cytoscape_props),
style={
"height": "100%",
"width": "100%",
"position": "absolute",
"top": 0,
"left": 0,
"zIndex": 2,
},
),
html.Div(
dl.Map(**leaflet_props),
style={
"height": "100%",
"width": "100%",
"position": "absolute",
"top": 0,
"left": 0,
"zIndex": 1,
},
),
dcc.Store(id=self.ids["elements"], data=elements),
],
style={
"width": width,
"height": height,
},
),
style={
"height": "100%",
"width": "100%",
"position": "relative",
},
)

def set_default_props_and_overrides(
self, user_cytoscape_props, user_leaflet_props, tiles
):
# If `tiles` is specified, append to end of Leaflet children
# This will make it the visible TileLayer
leaflet_children = user_leaflet_props.get("children", [])
if not isinstance(leaflet_children, list):
leaflet_children = [leaflet_children]
if tiles is not None:
leaflet_children.append(tiles)

# Try to figure out Leaflet maxZoom from Leaflet children,
# then convert to Cytoscape max zoom
leaflet_max_zoom = self.get_leaflet_max_zoom(leaflet_children)
cytoscape_max_zoom = self.get_cytoscape_max_zoom(leaflet_max_zoom)

# Props where we want to override values supplied by the user
# These are props which are required for CyLeaflet to work properly
cytoscape_overrides = {
"id": self.ids["cy"],
"elements": [], # Elements are set via clientside callback, so set to empty list initially
"layout": {"name": "preset", "fit": False},
"style": {"width": "100%", "height": "100%"},
"minZoom": 3 / 100000,
}
# Note: Leaflet MUST be initialized with a center and zoom to avoid an error,
# even though these will be immediately overwritten by syncing w/ Cytoscape
leaflet_overrides = {
"id": self.ids["leaf"],
"children": leaflet_children or [dl.TileLayer()],
"center": [0, 0],
"zoom": 6,
"zoomSnap": 0,
"zoomControl": False,
"zoomAnimation": False,
"maxZoom": 100000,
"maxBoundsViscosity": 1,
"maxBounds": [[-85, -180.0], [85, 180.0]],
"style": {"width": "100%", "height": "100%"},
}

# Props where we want to fill in a default value
# if a value is not supplied by the user
cytoscape_defaults = {
"boxSelectionEnabled": True,
"maxZoom": cytoscape_max_zoom,
}
leaflet_defaults = {}

# Start with default props
cytoscape_props = dict(cytoscape_defaults)
leaflet_props = dict(leaflet_defaults)

# Update with user-supplied props
cytoscape_props.update(user_cytoscape_props)
leaflet_props.update(user_leaflet_props)

# Update with overrides
cytoscape_props.update(cytoscape_overrides)
leaflet_props.update(leaflet_overrides)

return cytoscape_props, leaflet_props

# Try to figure out Leaflet maxZoom from Leaflet children
# If not possible, return the maxZoom of the default Leaflet tile layer
def get_leaflet_max_zoom(self, leaflet_children):
if leaflet_children is None or leaflet_children == []:
return LEAFLET_DEFAULT_MAX_ZOOM

max_zooms = [
c.maxZoom
for c in leaflet_children
if isinstance(c, dl.TileLayer) and hasattr(c, "maxZoom")
]

return max_zooms[-1] if len(max_zooms) > 0 else LEAFLET_DEFAULT_MAX_ZOOM

# Given a maxZoom value for Leaflet, map it to the corresponding maxZoom value for Cytoscape
# If the value is out of range, return the closest value
def get_cytoscape_max_zoom(self, leaflet_max_zoom):
leaflet_max_zoom = leaflet_max_zoom or 0
leaflet_max_zoom = min(
leaflet_max_zoom, max(LEAF_TO_CYTO_MAX_ZOOM_MAPPING.keys())
)
leaflet_max_zoom = max(
leaflet_max_zoom, min(LEAF_TO_CYTO_MAX_ZOOM_MAPPING.keys())
)
return LEAF_TO_CYTO_MAX_ZOOM_MAPPING[leaflet_max_zoom]


if dl is not None:
clientside_callback(
ClientsideFunction(namespace="cyleaflet", function_name="updateLeafBounds"),
Output(
{"id": MATCH, "component": "cyleaflet", "sub": "leaf"}, "invalidateSize"
),
Output({"id": MATCH, "component": "cyleaflet", "sub": "leaf"}, "viewport"),
Input({"id": MATCH, "component": "cyleaflet", "sub": "cy"}, "extent"),
)
clientside_callback(
ClientsideFunction(namespace="cyleaflet", function_name="transformElements"),
Output({"id": MATCH, "component": "cyleaflet", "sub": "cy"}, "elements"),
Input({"id": MATCH, "component": "cyleaflet", "sub": "elements"}, "data"),
)
9 changes: 9 additions & 0 deletions dash_cytoscape/Cytoscape.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,12 @@ class attribute).
- nodes (list; optional)
- extent (dict; optional):
Extent of the viewport, a bounding box in model co-ordinates that
lets you know what model positions are visible in the viewport.
This function returns a plain object bounding box with format {
x1, y1, x2, y2, w, h }.
- generateImage (dict; optional):
Dictionary specifying options to generate an image of the current
cytoscape graph. Value is cleared after data is received and image
Expand Down Expand Up @@ -540,6 +546,7 @@ def __init__(
generateImage=Component.UNDEFINED,
imageData=Component.UNDEFINED,
responsive=Component.UNDEFINED,
extent=Component.UNDEFINED,
clearOnUnhover=Component.UNDEFINED,
**kwargs
):
Expand All @@ -555,6 +562,7 @@ def __init__(
"contextMenu",
"contextMenuData",
"elements",
"extent",
"generateImage",
"imageData",
"layout",
Expand Down Expand Up @@ -591,6 +599,7 @@ def __init__(
"contextMenu",
"contextMenuData",
"elements",
"extent",
"generateImage",
"imageData",
"layout",
Expand Down
3 changes: 3 additions & 0 deletions dash_cytoscape/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
from ._imports_ import __all__
from . import utils

# Import CyLeaflet AIO component
from .CyLeaflet import CyLeaflet


if not hasattr(_dash, "__plotly_dash") and not hasattr(_dash, "development"):
print(
Expand Down
22 changes: 21 additions & 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.

4 changes: 4 additions & 0 deletions dash_cytoscape/dash_cytoscape.min.js.LICENSE.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ Copyright (c) 2013-2014 Ralf S. Engelschall (http://engelschall.com)
Licensed under The MIT License (http://opensource.org/licenses/MIT)
*/

/*! (c) Andrea Giammarchi - ISC */

/*! (c) Andrea Giammarchi @webreflection ISC */

/*! Bezier curve function generator. Copyright Gaetan Renaudeau. MIT License: http://en.wikipedia.org/wiki/MIT_License */

/*! Runge-Kutta spring physics function generator. Adapted from Framer.js, copyright Koen Bok. MIT License: http://en.wikipedia.org/wiki/MIT_License */
Expand Down
22 changes: 21 additions & 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.

4 changes: 4 additions & 0 deletions dash_cytoscape/dash_cytoscape_extra.min.js.LICENSE.txt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ Copyright (c) 2013-2014 Ralf S. Engelschall (http://engelschall.com)
Licensed under The MIT License (http://opensource.org/licenses/MIT)
*/

/*! (c) Andrea Giammarchi - ISC */

/*! (c) Andrea Giammarchi @webreflection ISC */

/*! Bezier curve function generator. Copyright Gaetan Renaudeau. MIT License: http://en.wikipedia.org/wiki/MIT_License */

/*! Runge-Kutta spring physics function generator. Adapted from Framer.js, copyright Koen Bok. MIT License: http://en.wikipedia.org/wiki/MIT_License */
Expand Down
14 changes: 14 additions & 0 deletions dash_cytoscape/metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,13 @@
}
],
"returns": null
},
{
"name": "graphOutOfView",
"docblock": null,
"modifiers": [],
"params": [],
"returns": null
}
],
"props": {
Expand Down Expand Up @@ -983,6 +990,13 @@
"computed": false
}
},
"extent": {
"type": {
"name": "object"
},
"required": false,
"description": "Extent of the viewport, a bounding box in model co-ordinates that lets you know what model\npositions are visible in the viewport. This function returns a plain object bounding box\nwith format { x1, y1, x2, y2, w, h }."
},
"clearOnUnhover": {
"type": {
"name": "bool"
Expand Down
5 changes: 3 additions & 2 deletions dash_cytoscape/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"author-email": "cytoscape@plotly.com",
"license": "MIT",
"dependencies": {
"@ungap/custom-elements": "^1.3.0",
"cytoscape-cola": "^2.5.1",
"cytoscape-context-menus": "^4.1.0",
"cytoscape-cose-bilkent": "^4.1.0",
Expand All @@ -56,9 +57,9 @@
"devDependencies": {
"@babel/core": "^7.23.0",
"@babel/eslint-parser": "^7.22.15",
"babel-loader": "^9.1.3",
"@babel/preset-env": "^7.22.20",
"@babel/preset-react": "^7.22.15",
"babel-loader": "^9.1.3",
"copyfiles": "^2.4.1",
"css-loader": "^6.8.1",
"eslint": "^8.50.0",
Expand All @@ -82,4 +83,4 @@
"node": ">=8.11.0",
"npm": ">=6.1.0"
}
}
}

0 comments on commit cdfea33

Please sign in to comment.