Skip to content

Commit

Permalink
Merge pull request #216 from plotly/bugfix/update-maxzoom-cyleaflet
Browse files Browse the repository at this point in the history
Bugfix/update maxzoom cyleaflet
  • Loading branch information
Farkites authored May 16, 2024
2 parents 77817ed + b1cbb51 commit 67fa01f
Show file tree
Hide file tree
Showing 19 changed files with 216 additions and 59 deletions.
2 changes: 1 addition & 1 deletion .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ What does this implement/fix? Explain your changes.
- [ ] All changes were documented in CHANGELOG.md.
- [ ] All tests on CircleCI have passed.
- [ ] All Percy visual changes have been approved.
- [ ] Two people have :dancer:'d the pull request. You can be one of these people if you are a Dash Cytoscape core contributor.
- [ ] At least one person has :dancer:'d the pull request.


## Reference Issues
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Fixed
* [#205](https://github.com/plotly/dash-cytoscape/pull/205) Fixed updating maxZoom via callback in CyLeaflet AIO component.
* [#207](https://github.com/plotly/dash-cytoscape/pull/207) Allow access to updated lat/lon when Cytoscape nodes in CyLeaflet AIO component are modified via UI.
* [#208](https://github.com/plotly/dash-cytoscape/pull/208) Allow updating Cytoscape elements in CyLeaflet AIO component via callback.

## [1.0.0] - 2024-01-26

### Removed
Expand Down
54 changes: 33 additions & 21 deletions dash_cytoscape/CyLeaflet.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@
ClientsideFunction,
Output,
Input,
State,
html,
dcc,
MATCH,
no_update,
)

import dash_cytoscape as cyto
import math


try:
import dash_leaflet as dl
Expand All @@ -19,17 +23,12 @@
# 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,
}

# Approximates max zoom values for Cytoscape from max zoom
# values of Leaflet. Empirically-determined
def get_cytoscape_max_zoom(leaflet_max_zoom):
leaflet_max_zoom = leaflet_max_zoom or 0
return 0.418 * (2 ** (leaflet_max_zoom - 16))


class CyLeaflet(html.Div):
Expand Down Expand Up @@ -62,7 +61,7 @@ def __init__(

self.ids = {
s: {"id": id, "component": "cyleaflet", "sub": s}
for s in ["cy", "leaf", "elements"]
for s in ["cy", "leaf", "elements", "cy-extent"]
}

self.CYTOSCAPE_ID = self.ids["cy"]
Expand Down Expand Up @@ -102,6 +101,7 @@ def __init__(
},
),
dcc.Store(id=self.ids["elements"], data=elements),
dcc.Store(id=self.ids["cy-extent"]),
],
style={
"width": width,
Expand Down Expand Up @@ -195,14 +195,7 @@ def get_leaflet_max_zoom(self, leaflet_children):
# 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]
return get_cytoscape_max_zoom(leaflet_max_zoom)


if dl is not None:
Expand All @@ -212,10 +205,29 @@ def get_cytoscape_max_zoom(self, leaflet_max_zoom):
{"id": MATCH, "component": "cyleaflet", "sub": "leaf"}, "invalidateSize"
),
Output({"id": MATCH, "component": "cyleaflet", "sub": "leaf"}, "viewport"),
Output({"id": MATCH, "component": "cyleaflet", "sub": "cy-extent"}, "data"),
Input({"id": MATCH, "component": "cyleaflet", "sub": "cy"}, "extent"),
Input({"id": MATCH, "component": "cyleaflet", "sub": "cy"}, "maxZoom"),
State({"id": MATCH, "component": "cyleaflet", "sub": "cy-extent"}, "data"),
)
clientside_callback(
ClientsideFunction(namespace="cyleaflet", function_name="transformElements"),
Output({"id": MATCH, "component": "cyleaflet", "sub": "cy"}, "elements"),
Output(
{"id": MATCH, "component": "cyleaflet", "sub": "cy"},
"elements",
allow_duplicate=True,
),
Input({"id": MATCH, "component": "cyleaflet", "sub": "elements"}, "data"),
prevent_initial_call="initial_duplicate",
)
clientside_callback(
ClientsideFunction(namespace="cyleaflet", function_name="updateLonLat"),
Output({"id": MATCH, "component": "cyleaflet", "sub": "elements"}, "data"),
Input({"id": MATCH, "component": "cyleaflet", "sub": "cy"}, "elements"),
prevent_initial_call=True,
)
clientside_callback(
ClientsideFunction(namespace="cyleaflet", function_name="updateCytoMaxZoom"),
Output({"id": MATCH, "component": "cyleaflet", "sub": "cy"}, "maxZoom"),
Input({"id": MATCH, "component": "cyleaflet", "sub": "leaf"}, "children"),
)
4 changes: 2 additions & 2 deletions 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: 2 additions & 2 deletions 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.

3 changes: 3 additions & 0 deletions dash_cytoscape/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@
"webpack": "^5.90.0",
"webpack-cli": "^5.1.4"
},
"files": [
"/dash_cytoscape/*{.js,.map,.txt}"
],
"engines": {
"node": ">=8.11.0",
"npm": ">=6.1.0"
Expand Down
67 changes: 54 additions & 13 deletions demos/usage-cy-leaflet-aio.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
)
import dash_cytoscape as cyto
import dash_leaflet as dl
import random

cyto.load_extra_layouts()

Expand Down Expand Up @@ -80,42 +81,82 @@
options=location_options,
value=location_options[0]["value"],
),
"Width",
dcc.Input(
id="width-input",
type="number",
value=int(default_div_style["width"][:-2]),
debounce=True,
),
"Height",
dcc.Input(
id="height-input",
type="number",
value=int(default_div_style["height"][:-2]),
debounce=True,
),
"Max zoom",
dcc.Input(id="max-zoom", type="number", value=18, debounce=True),
"Number of Nodes",
dcc.Input(id="n-nodes", type="number", value=4, debounce=True),
],
),
html.Div(id="elements"),
],
)


@callback(Output("elements", "children"), Input(cyleaflet_instance.ELEMENTS_ID, "data"))
def show_elements(elements):
return str(elements)


def generate_elements(n_nodes, location):
d = 0.00005
lat, lon = city_lat_lon[location]
if n_nodes < 2:
n_nodes = 2
elif n_nodes > 10000:
n_nodes = 10000

elements = []
for i in range(n_nodes):
rand_lat, rand_lon = random.randint(-5, 5) * i, random.randint(-5, 5) * i
elements.append(
{
"data": {
"id": f"{i}",
"label": f"Node {i}",
"lat": lat + d * rand_lat,
"lon": lon + d * rand_lon,
}
}
)
elements.append({"data": {"id": "0-1", "source": "0", "target": "1"}})
return elements


@callback(
Output(cyleaflet_instance.CYTOSCAPE_ID, "elements", allow_duplicate=True),
Input("n-nodes", "value"),
Input("location-dropdown", "value"),
prevent_initial_call=True,
)
def control_number_nodes(n_nodes, location):
return generate_elements(n_nodes, location)


@callback(
Output("cy-leaflet-div", "children"),
Output("cy-leaflet-div", "style"),
Output(cyleaflet_instance.LEAFLET_ID, "children"),
Input("location-dropdown", "value"),
Input("width-input", "value"),
Input("height-input", "value"),
Input("max-zoom", "value"),
)
def update_location(location, width, height):
d = 0.001
d2 = 0.0001
lat, lon = city_lat_lon[location]
new_elements = [
{"data": {"id": "a", "label": "Node A", "lat": lat - d, "lon": lon - d}},
{"data": {"id": "b", "label": "Node B", "lat": lat + d, "lon": lon + d}},
{"data": {"id": "c", "label": "Node C", "lat": lat + d - d2, "lon": lon + d}},
{"data": {"id": "ab", "source": "a", "target": "b"}},
]
def update_location(location, width, height, max_zoom):
new_elements = generate_elements(4, location)
markers = [
dl.Marker(
position=[e["data"]["lat"], e["data"]["lon"]],
Expand All @@ -128,7 +169,7 @@ def update_location(location, width, height):
for e in new_elements
if "lat" in e["data"]
]
leaflet_children = [dl.TileLayer()] + markers
leaflet_children = [dl.TileLayer(maxZoom=max_zoom)] + markers
new_style = dict(default_div_style)
new_style["width"] = str(width) + "px" if width else default_div_style["width"]
new_style["height"] = str(height) + "px" if height else default_div_style["height"]
Expand All @@ -150,8 +191,8 @@ def update_location(location, width, height):
@callback(
Output("bounds-display", "children"),
Output("extent-display", "children"),
Input({"id": "my-cy-leaflet", "sub": "leaf", "component": "cyleaflet"}, "bounds"),
Input({"id": "my-cy-leaflet", "sub": "cy", "component": "cyleaflet"}, "extent"),
Input(cyleaflet_instance.LEAFLET_ID, "bounds"),
Input(cyleaflet_instance.CYTOSCAPE_ID, "extent"),
)
def display_leaf_bounds(bounds, extent):
bounds = (
Expand Down
4 changes: 2 additions & 2 deletions deps/dash_cytoscape.dev.js

Large diffs are not rendered by default.

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

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions deps/dash_cytoscape_extra.dev.js

Large diffs are not rendered by default.

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

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions inst/deps/dash_cytoscape.dev.js

Large diffs are not rendered by default.

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

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions inst/deps/dash_cytoscape_extra.dev.js

Large diffs are not rendered by default.

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

Large diffs are not rendered by default.

38 changes: 38 additions & 0 deletions src/lib/components/Cytoscape.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,36 @@ class Cytoscape extends Component {
});
}, EXTENT_THRESHOLD);

// Store the original maxZoom and minZoom functions
const originalMaxZoomFn = cy.maxZoom;
const originalMinZoomFn = cy.minZoom;

// Override the maxZoom function to trigger maxZoomChange custom event
cy.maxZoom = function (e) {
const currentMaxZoom = originalMaxZoomFn.call(cy, e);

// Trigger your custom event if the current max zoom level is different from the
// previously stored max zoom level
if (currentMaxZoom !== cy._previousMaxZoom) {
cy._previousMaxZoom = currentMaxZoom;
cy.trigger('minMaxZoomChange');
}
return currentMaxZoom;
};

// Override the minZoom function to trigger minZoomChange custom event
cy.minZoom = function (e) {
const currentMinZoom = originalMinZoomFn.call(cy, e);

// Trigger your custom event if the current min zoom level is different from the
// previously stored min zoom level
if (currentMinZoom !== cy._previousMinZoom) {
cy._previousMinZoom = currentMinZoom;
cy.trigger('minMaxZoomChange');
}
return currentMinZoom;
};

// /////////////////////////////////////// EVENTS //////////////////////////////////////////

cy.on('tap', 'node', (event) => {
Expand Down Expand Up @@ -321,6 +351,14 @@ class Cytoscape extends Component {
setExtent(cy.extent());
});

// Refresh layout if current zoom is out of boundaries
cy.on('minMaxZoomChange', function () {
const zoom = cy.zoom();
if (zoom > cy.maxZoom() || zoom < cy.minZoom()) {
cy.fit();
}
});

this.createMenuItems = (ctxMenu) => {
const updateContextMenuData = (newContext) => {
this.props.setProps({contextMenuData: newContext});
Expand Down
Loading

0 comments on commit 67fa01f

Please sign in to comment.