-
Notifications
You must be signed in to change notification settings - Fork 2.2k
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
Feature overlapping markers #2033
Merged
Conengmo
merged 10 commits into
python-visualization:main
from
swtormy:feature-overlapping-markers
Nov 27, 2024
Merged
Changes from 3 commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
5c245ef
feat(plugins): add OverlappingMarkerSpiderfier plugin for handling ov…
swtormy c4dd6d2
docs(plugins): update overlapping_marker_spiderfier to align with plu…
swtormy 7ec3548
docs(overlapping_marker_spiderfier): add class description
swtormy bcd5948
refactor(plugin): simplify marker and popup handling in OMS
swtormy 07e536d
fix: resolve pre-commit issues
swtormy 3ea2a7f
Update folium/plugins/overlapping_marker_spiderfier.py
swtormy d8c6018
Update folium/plugins/overlapping_marker_spiderfier.py
swtormy 42eefa2
feat: add support for spiderifying markers in FeatureGroups
swtormy eb898c2
docs: modification of OverlappingMarkerSpiderfier plugin documentation
swtormy 6ec0e09
Update folium/plugins/overlapping_marker_spiderfier.py
Conengmo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
# OverlappingMarkerSpiderfier | ||
|
||
```{code-cell} ipython3 | ||
import folium | ||
from folium import plugins | ||
|
||
# Create a map | ||
m = folium.Map(location=[45.05, 3.05], zoom_start=14) | ||
|
||
# Generate some markers | ||
markers = [folium.Marker(location=[45.05 + i * 0.0001, 3.05 + i * 0.0001], options={'desc': f'Marker {i}'}) for i in range(10)] | ||
|
||
# Add markers to the map | ||
for marker in markers: | ||
marker.add_to(m) | ||
|
||
# Add OverlappingMarkerSpiderfier | ||
oms = plugins.OverlappingMarkerSpiderfier( | ||
markers=markers, | ||
options={'keepSpiderfied': True, 'nearbyDistance': 20} | ||
).add_to(m) | ||
|
||
# Display the map | ||
m |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
from jinja2 import Template | ||
|
||
from folium.elements import JSCSSMixin | ||
from folium.map import Layer | ||
from folium.utilities import parse_options | ||
|
||
|
||
class OverlappingMarkerSpiderfier(JSCSSMixin, Layer): | ||
"""A plugin that handles overlapping markers by spreading them into a spider-like pattern. | ||
|
||
This plugin uses the OverlappingMarkerSpiderfier-Leaflet library to manage markers | ||
that are close to each other or overlap. When clicked, the overlapping markers | ||
spread out in a spiral pattern, making them easier to select individually. | ||
|
||
Parameters | ||
---------- | ||
markers : list, optional | ||
List of markers to be managed by the spiderfier | ||
name : string, optional | ||
Name of the layer control | ||
overlay : bool, default True | ||
Whether the layer will be included in LayerControl | ||
control : bool, default True | ||
Whether the layer will be included in LayerControl | ||
show : bool, default True | ||
Whether the layer will be shown on opening | ||
options : dict, optional | ||
Additional options to be passed to the OverlappingMarkerSpiderfier instance | ||
See https://github.com/jawj/OverlappingMarkerSpiderfier-Leaflet for available options | ||
|
||
Example | ||
------- | ||
>>> markers = [marker1, marker2, marker3] # Create some markers | ||
>>> spiderfier = OverlappingMarkerSpiderfier( | ||
... markers=markers, keepSpiderfied=True, nearbyDistance=20 | ||
... ) | ||
>>> spiderfier.add_to(m) # Add to your map | ||
""" | ||
|
||
_template = Template( | ||
""" | ||
{% macro script(this, kwargs) %} | ||
var {{ this.get_name() }} = (function () { | ||
var layerGroup = L.layerGroup(); | ||
|
||
try { | ||
var oms = new OverlappingMarkerSpiderfier( | ||
{{ this._parent.get_name() }}, | ||
{{ this.options|tojson }} | ||
); | ||
|
||
var popup = L.popup({ | ||
offset: L.point(0, -30) | ||
}); | ||
|
||
oms.addListener('click', function(marker) { | ||
var content; | ||
if (marker.options && marker.options.options && marker.options.options.desc) { | ||
content = marker.options.options.desc; | ||
} else if (marker._popup && marker._popup._content) { | ||
content = marker._popup._content; | ||
} else { | ||
content = ""; | ||
} | ||
|
||
if (content) { | ||
popup.setContent(content); | ||
popup.setLatLng(marker.getLatLng()); | ||
{{ this._parent.get_name() }}.openPopup(popup); | ||
} | ||
}); | ||
|
||
oms.addListener('spiderfy', function(markers) { | ||
{{ this._parent.get_name() }}.closePopup(); | ||
}); | ||
|
||
{% for marker in this.markers %} | ||
var {{ marker.get_name() }} = L.marker( | ||
{{ marker.location|tojson }}, | ||
{{ marker.options|tojson }} | ||
); | ||
|
||
{% if marker.popup %} | ||
{{ marker.get_name() }}.bindPopup({{ marker.popup.get_content()|tojson }}); | ||
{% endif %} | ||
|
||
oms.addMarker({{ marker.get_name() }}); | ||
layerGroup.addLayer({{ marker.get_name() }}); | ||
{% endfor %} | ||
} catch (error) { | ||
console.error('Error in OverlappingMarkerSpiderfier initialization:', error); | ||
} | ||
|
||
return layerGroup; | ||
})(); | ||
{% endmacro %} | ||
|
||
""" | ||
) | ||
|
||
default_js = [ | ||
( | ||
"overlappingmarkerjs", | ||
"https://cdnjs.cloudflare.com/ajax/libs/OverlappingMarkerSpiderfier-Leaflet/0.2.6/oms.min.js", | ||
) | ||
] | ||
|
||
def __init__( | ||
self, | ||
markers=None, | ||
name=None, | ||
overlay=True, | ||
control=True, | ||
show=True, | ||
options=None, | ||
**kwargs, | ||
): | ||
super().__init__(name=name, overlay=overlay, control=control, show=show) | ||
self._name = "OverlappingMarkerSpiderfier" | ||
|
||
self.markers = markers or [] | ||
|
||
default_options = { | ||
"keepSpiderfied": True, | ||
"nearbyDistance": 20, | ||
"legWeight": 1.5, | ||
} | ||
if options: | ||
default_options.update(options) | ||
|
||
self.options = parse_options(**default_options, **kwargs) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
""" | ||
Test OverlappingMarkerSpiderfier | ||
-------------------------------- | ||
""" | ||
|
||
import numpy as np | ||
from jinja2 import Template | ||
|
||
import folium | ||
from folium import plugins | ||
from folium.utilities import normalize | ||
|
||
|
||
def test_overlapping_marker_spiderfier(): | ||
N = 10 | ||
np.random.seed(seed=26082009) | ||
data = np.array( | ||
[ | ||
np.random.uniform(low=45.0, high=45.1, size=N), | ||
np.random.uniform(low=3.0, high=3.1, size=N), | ||
] | ||
).T | ||
|
||
m = folium.Map([45.05, 3.05], zoom_start=14) | ||
markers = [ | ||
folium.Marker(location=loc, popup=f"Marker {i}") for i, loc in enumerate(data) | ||
] | ||
|
||
for marker in markers: | ||
marker.add_to(m) | ||
|
||
oms = plugins.OverlappingMarkerSpiderfier( | ||
markers=markers, options={"keepSpiderfied": True, "nearbyDistance": 20} | ||
).add_to(m) | ||
|
||
tmpl_for_expected = Template( | ||
""" | ||
var {{this.get_name()}} = (function () { | ||
var layerGroup = L.layerGroup(); | ||
try { | ||
var oms = new OverlappingMarkerSpiderfier( | ||
{{ this._parent.get_name() }}, | ||
{{ this.options|tojson }} | ||
); | ||
|
||
var popup = L.popup({ | ||
offset: L.point(0, -30) | ||
}); | ||
|
||
oms.addListener('click', function(marker) { | ||
var content; | ||
if (marker.options && marker.options.options && marker.options.options.desc) { | ||
content = marker.options.options.desc; | ||
} else if (marker._popup && marker._popup._content) { | ||
content = marker._popup._content; | ||
} else { | ||
content = ""; | ||
} | ||
|
||
if (content) { | ||
popup.setContent(content); | ||
popup.setLatLng(marker.getLatLng()); | ||
{{ this._parent.get_name() }}.openPopup(popup); | ||
} | ||
}); | ||
|
||
oms.addListener('spiderfy', function(markers) { | ||
{{ this._parent.get_name() }}.closePopup(); | ||
}); | ||
|
||
{% for marker in this.markers %} | ||
var {{ marker.get_name() }} = L.marker( | ||
{{ marker.location|tojson }}, | ||
{{ marker.options|tojson }} | ||
); | ||
|
||
{% if marker.popup %} | ||
{{ marker.get_name() }}.bindPopup({{ marker.popup.get_content()|tojson }}); | ||
{% endif %} | ||
|
||
oms.addMarker({{ marker.get_name() }}); | ||
layerGroup.addLayer({{ marker.get_name() }}); | ||
{% endfor %} | ||
} catch (error) { | ||
console.error('Error in OverlappingMarkerSpiderfier initialization:', error); | ||
} | ||
|
||
return layerGroup; | ||
})(); | ||
""" | ||
) | ||
expected = normalize(tmpl_for_expected.render(this=oms)) | ||
|
||
out = normalize(m._parent.render()) | ||
|
||
assert ( | ||
'<script src="https://cdnjs.cloudflare.com/ajax/libs/OverlappingMarkerSpiderfier-Leaflet/0.2.6/oms.min.js"></script>' | ||
in out | ||
) | ||
|
||
assert expected in out | ||
|
||
bounds = m.get_bounds() | ||
assert bounds is not None, "Map bounds should not be None" | ||
|
||
min_lat, min_lon = data.min(axis=0) | ||
max_lat, max_lon = data.max(axis=0) | ||
|
||
assert bounds[0][0] <= min_lat | ||
assert bounds[0][1] <= min_lon | ||
assert bounds[1][0] >= max_lat | ||
assert bounds[1][1] >= max_lon |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really like how you used an IFFE here. I think we should use these for all our templates.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi! I looked up this method in FastMarkerCluster, so at least one plugin already uses IFFE)