Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CyLeaflet AIO component #200

Merged
merged 48 commits into from
Jan 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
d0449a0
adding AIO leaflet component
cleaaum Nov 24, 2023
989c3c5
adjusting map scaling
cleaaum Nov 24, 2023
35472bc
adding missing proptypes
cleaaum Nov 24, 2023
c3a5cf1
adding basic ctx menu to the demo
cleaaum Nov 24, 2023
efba245
remove unused imports
cleaaum Nov 24, 2023
03541db
rebase
cleaaum Dec 1, 2023
aecefbb
rename DashCyLeaflet to CyLeaflet and move from demos folder to dash_…
emilykl Dec 4, 2023
eb88d6d
move clientside cyleaflet functions to src directory
emilykl Dec 5, 2023
c9b6a8a
add proj4 dependency
emilykl Dec 5, 2023
254ffda
update CyLeaflet to work with dash-leaflet 1.0.x
emilykl Dec 5, 2023
97eafa6
rename elements store in CyLeaflet
emilykl Dec 5, 2023
a4278b8
change dl.Map to dl.MapContainer
emilykl Dec 7, 2023
81a4f3c
slightly smoother redraw on map drag
emilykl Dec 7, 2023
edcdd35
bugfix + remove ctx menu
cleaaum Dec 8, 2023
4d3f4ce
build
cleaaum Dec 8, 2023
60201aa
fix bug where resizing CyLeaflet div causes misalignment
emilykl Dec 19, 2023
824bc0b
Merge branch 'leaflet-aio' of https://github.com/plotly/dash-cytoscap…
emilykl Dec 19, 2023
4e8cfb5
reset Cyto view on element update if new elements are entirely outsid…
emilykl Dec 20, 2023
20fb526
improved init logic for CyLeaflet
emilykl Dec 20, 2023
7089108
restrict cytoscape zoom level
cleaaum Dec 20, 2023
7dd1063
bugfix for keyerror
cleaaum Dec 20, 2023
2a553a5
debounce threshold for extent update
emilykl Dec 20, 2023
8f62973
no more map flicker
emilykl Dec 20, 2023
ccd20b8
Merge branch 'leaflet-aio' of https://github.com/plotly/dash-cytoscap…
emilykl Dec 20, 2023
d804e28
remove unneeded lines
emilykl Dec 20, 2023
56e8a2d
adding width and height as params to run demo without them
cleaaum Dec 22, 2023
3b6d50d
update demo with new height/width params
cleaaum Dec 26, 2023
b31bbf9
putting clienside cbs in root level of file
cleaaum Jan 5, 2024
fb47ecc
add dash-leaflet as optional dependency
emilykl Jan 6, 2024
6c22b35
fix CyLeaflet loading in docs
emilykl Jan 7, 2024
05a9f2d
more specific pattern-matching ids
emilykl Jan 7, 2024
3879ef2
set minimum dash_leaflet version
emilykl Jan 7, 2024
5cdc535
build artifacts
emilykl Jan 7, 2024
c74ad7b
adding missing IDs to demo
cleaaum Jan 8, 2024
9d650c3
make Cytoscape useable without installing dash-leaflet
emilykl Jan 8, 2024
27dc261
chaning maxZoom as default prop
cleaaum Jan 8, 2024
14a9722
fix graphOutOfView() behavior
emilykl Jan 12, 2024
d28506f
Merge branch 'leaflet-aio' of https://github.com/plotly/dash-cytoscap…
emilykl Jan 12, 2024
97d89a8
more intelligent maxZoom behavior
emilykl Jan 16, 2024
17d9e62
fix fitting behavior
emilykl Jan 16, 2024
7eeb7fc
add predefined OSM tile layer and tiles argument for CyLeaflet
emilykl Jan 17, 2024
4b6de95
make clientside helpers local variables; update comments
emilykl Jan 18, 2024
88e6640
inline coordinate conversion functions instead of importing proj4js
emilykl Jan 19, 2024
e90b7c5
update build artifacts
emilykl Jan 19, 2024
ec1a55d
Simplify conversion function
emilykl Jan 19, 2024
0249069
Cleanup
emilykl Jan 19, 2024
45bcbe6
Merge branch 'leaflet-aio' of https://github.com/plotly/dash-cytoscap…
emilykl Jan 19, 2024
0620d28
Cleanup
emilykl Jan 20, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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"
}
}
}