From 5c245efdf4efb89895189e2b670db9e0573831c3 Mon Sep 17 00:00:00 2001 From: swtormy Date: Sat, 16 Nov 2024 08:56:06 +0300 Subject: [PATCH 01/10] feat(plugins): add OverlappingMarkerSpiderfier plugin for handling overlapping markers --- .../plugins/overlapping_marker_spiderfier.md | 59 +++++++++ folium/plugins/__init__.py | 2 + .../plugins/overlapping_marker_spiderfier.py | 100 ++++++++++++++++ .../test_overlapping_marker_spiderfier.py | 112 ++++++++++++++++++ 4 files changed, 273 insertions(+) create mode 100644 docs/user_guide/plugins/overlapping_marker_spiderfier.md create mode 100644 folium/plugins/overlapping_marker_spiderfier.py create mode 100644 tests/plugins/test_overlapping_marker_spiderfier.py diff --git a/docs/user_guide/plugins/overlapping_marker_spiderfier.md b/docs/user_guide/plugins/overlapping_marker_spiderfier.md new file mode 100644 index 0000000000..e7dd28be1f --- /dev/null +++ b/docs/user_guide/plugins/overlapping_marker_spiderfier.md @@ -0,0 +1,59 @@ +# OverlappingMarkerSpiderfier + +The `OverlappingMarkerSpiderfier` plugin for Folium is designed to handle overlapping markers on a map. When multiple markers are located at the same or nearby coordinates, they can overlap, making it difficult to interact with individual markers. This plugin "spiderfies" the markers, spreading them out in a spider-like pattern, allowing users to easily click and view each marker. + +## Features + +- **Spiderfying**: Automatically spreads out overlapping markers into a spider-like pattern when clicked, making them individually accessible. +- **Customizable Options**: Offers options to customize the behavior and appearance of the spiderfied markers, such as `keepSpiderfied`, `nearbyDistance`, and `legWeight`. +- **Popup Integration**: Supports popups for each marker, which can be customized to display additional information. +- **Layer Control**: Can be added as a layer to the map, allowing users to toggle its visibility. + +## Usage + +To use the `OverlappingMarkerSpiderfier`, you need to create a list of `folium.Marker` objects and pass them to the plugin. You can also customize the options to suit your needs. + +### Example + +```python +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 +``` + +## Options + +- **keepSpiderfied**: (bool) Whether to keep the markers spiderfied after clicking. +- **nearbyDistance**: (int) The distance in pixels within which markers are considered overlapping. +- **legWeight**: (float) The weight of the spider legs connecting the markers. + +## Installation + +Ensure you have Folium installed in your Python environment. You can install it using pip: + +```bash +pip install folium +``` + +## Conclusion + +The `OverlappingMarkerSpiderfier` plugin is a powerful tool for managing overlapping markers on a map, enhancing the user experience by making it easier to interact with individual markers. Customize it to fit your application's needs and improve the clarity of your map visualizations. diff --git a/folium/plugins/__init__.py b/folium/plugins/__init__.py index ad857f48ac..59e89b883c 100644 --- a/folium/plugins/__init__.py +++ b/folium/plugins/__init__.py @@ -19,6 +19,7 @@ from folium.plugins.measure_control import MeasureControl from folium.plugins.minimap import MiniMap from folium.plugins.mouse_position import MousePosition +from folium.plugins.overlapping_marker_spiderfier import OverlappingMarkerSpiderfier from folium.plugins.pattern import CirclePattern, StripePattern from folium.plugins.polyline_offset import PolyLineOffset from folium.plugins.polyline_text_path import PolyLineTextPath @@ -56,6 +57,7 @@ "MeasureControl", "MiniMap", "MousePosition", + "OverlappingMarkerSpiderfier", "PolygonFromEncoded", "PolyLineFromEncoded", "PolyLineTextPath", diff --git a/folium/plugins/overlapping_marker_spiderfier.py b/folium/plugins/overlapping_marker_spiderfier.py new file mode 100644 index 0000000000..f7184adb69 --- /dev/null +++ b/folium/plugins/overlapping_marker_spiderfier.py @@ -0,0 +1,100 @@ +from jinja2 import Template + +from folium.elements import JSCSSMixin +from folium.map import Layer +from folium.utilities import parse_options + + +class OverlappingMarkerSpiderfier(JSCSSMixin, Layer): + _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) diff --git a/tests/plugins/test_overlapping_marker_spiderfier.py b/tests/plugins/test_overlapping_marker_spiderfier.py new file mode 100644 index 0000000000..204ba365de --- /dev/null +++ b/tests/plugins/test_overlapping_marker_spiderfier.py @@ -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 ( + '' + 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 From c4dd6d239e713e1f62f786b9c60ccbcfc90ea517 Mon Sep 17 00:00:00 2001 From: swtormy Date: Sat, 16 Nov 2024 10:38:25 +0300 Subject: [PATCH 02/10] docs(plugins): update overlapping_marker_spiderfier to align with plugin conventions --- docs/user_guide/plugins.rst | 1 + .../plugins/overlapping_marker_spiderfier.md | 37 +------------------ 2 files changed, 2 insertions(+), 36 deletions(-) diff --git a/docs/user_guide/plugins.rst b/docs/user_guide/plugins.rst index e9e18324f9..947a1ea356 100644 --- a/docs/user_guide/plugins.rst +++ b/docs/user_guide/plugins.rst @@ -21,6 +21,7 @@ Plugins plugins/mini_map plugins/measure_control plugins/mouse_position + plugins/overlapping_marker_spiderfier plugins/pattern plugins/polygon_encoded plugins/polyline_encoded diff --git a/docs/user_guide/plugins/overlapping_marker_spiderfier.md b/docs/user_guide/plugins/overlapping_marker_spiderfier.md index e7dd28be1f..4073f80da9 100644 --- a/docs/user_guide/plugins/overlapping_marker_spiderfier.md +++ b/docs/user_guide/plugins/overlapping_marker_spiderfier.md @@ -1,21 +1,6 @@ # OverlappingMarkerSpiderfier -The `OverlappingMarkerSpiderfier` plugin for Folium is designed to handle overlapping markers on a map. When multiple markers are located at the same or nearby coordinates, they can overlap, making it difficult to interact with individual markers. This plugin "spiderfies" the markers, spreading them out in a spider-like pattern, allowing users to easily click and view each marker. - -## Features - -- **Spiderfying**: Automatically spreads out overlapping markers into a spider-like pattern when clicked, making them individually accessible. -- **Customizable Options**: Offers options to customize the behavior and appearance of the spiderfied markers, such as `keepSpiderfied`, `nearbyDistance`, and `legWeight`. -- **Popup Integration**: Supports popups for each marker, which can be customized to display additional information. -- **Layer Control**: Can be added as a layer to the map, allowing users to toggle its visibility. - -## Usage - -To use the `OverlappingMarkerSpiderfier`, you need to create a list of `folium.Marker` objects and pass them to the plugin. You can also customize the options to suit your needs. - -### Example - -```python +```{code-cell} ipython3 import folium from folium import plugins @@ -25,7 +10,6 @@ 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) @@ -38,22 +22,3 @@ oms = plugins.OverlappingMarkerSpiderfier( # Display the map m -``` - -## Options - -- **keepSpiderfied**: (bool) Whether to keep the markers spiderfied after clicking. -- **nearbyDistance**: (int) The distance in pixels within which markers are considered overlapping. -- **legWeight**: (float) The weight of the spider legs connecting the markers. - -## Installation - -Ensure you have Folium installed in your Python environment. You can install it using pip: - -```bash -pip install folium -``` - -## Conclusion - -The `OverlappingMarkerSpiderfier` plugin is a powerful tool for managing overlapping markers on a map, enhancing the user experience by making it easier to interact with individual markers. Customize it to fit your application's needs and improve the clarity of your map visualizations. From 7ec354887f131b1843e956415fc3bc519549ffc1 Mon Sep 17 00:00:00 2001 From: swtormy Date: Sat, 16 Nov 2024 10:55:16 +0300 Subject: [PATCH 03/10] docs(overlapping_marker_spiderfier): add class description --- .../plugins/overlapping_marker_spiderfier.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/folium/plugins/overlapping_marker_spiderfier.py b/folium/plugins/overlapping_marker_spiderfier.py index f7184adb69..90ef8bfc7c 100644 --- a/folium/plugins/overlapping_marker_spiderfier.py +++ b/folium/plugins/overlapping_marker_spiderfier.py @@ -6,6 +6,37 @@ 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) %} From bcd59485e3d16b5d26786c831861c9723c8664e3 Mon Sep 17 00:00:00 2001 From: swtormy Date: Mon, 18 Nov 2024 10:16:41 +0300 Subject: [PATCH 04/10] refactor(plugin): simplify marker and popup handling in OMS --- .../plugins/overlapping_marker_spiderfier.md | 32 ++-- .../plugins/overlapping_marker_spiderfier.py | 124 +++++---------- .../test_overlapping_marker_spiderfier.py | 142 ++++++++---------- 3 files changed, 121 insertions(+), 177 deletions(-) diff --git a/docs/user_guide/plugins/overlapping_marker_spiderfier.md b/docs/user_guide/plugins/overlapping_marker_spiderfier.md index 4073f80da9..8895bdc823 100644 --- a/docs/user_guide/plugins/overlapping_marker_spiderfier.md +++ b/docs/user_guide/plugins/overlapping_marker_spiderfier.md @@ -1,24 +1,30 @@ # OverlappingMarkerSpiderfier +The `OverlappingMarkerSpiderfier` is a plugin for Folium that helps manage overlapping markers by "spiderfying" them when clicked, making it easier to select individual markers. + ```{code-cell} ipython3 import folium -from folium import plugins +from folium.plugins import OverlappingMarkerSpiderfier # 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)] +m = folium.Map(location=[45.05, 3.05], zoom_start=13) # Add markers to the map -for marker in markers: - marker.add_to(m) +for i in range(20): + folium.Marker( + location=[45.05 + i * 0.0001, 3.05 + i * 0.0001], + popup=f"Marker {i}" + ).add_to(m) -# Add OverlappingMarkerSpiderfier -oms = plugins.OverlappingMarkerSpiderfier( - markers=markers, - options={'keepSpiderfied': True, 'nearbyDistance': 20} -).add_to(m) +# Add the OverlappingMarkerSpiderfier plugin +oms = OverlappingMarkerSpiderfier(options={ + "keepSpiderfied": True, # Markers remain spiderfied after clicking + "nearbyDistance": 20, # Distance for clustering markers in pixel + "circleSpiralSwitchover": 10, # Threshold for switching between circle and spiral + "legWeight": 2.0 # Line thickness for spider legs +}) +oms.add_to(m) -# Display the map m +``` + diff --git a/folium/plugins/overlapping_marker_spiderfier.py b/folium/plugins/overlapping_marker_spiderfier.py index 90ef8bfc7c..b3c5bbda57 100644 --- a/folium/plugins/overlapping_marker_spiderfier.py +++ b/folium/plugins/overlapping_marker_spiderfier.py @@ -1,100 +1,67 @@ from jinja2 import Template - -from folium.elements import JSCSSMixin -from folium.map import Layer +from folium.elements import JSCSSMixin, MacroElement 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. - +class OverlappingMarkerSpiderfier(JSCSSMixin, MacroElement): + """ + A plugin that handles overlapping markers on a map by spreading them out in a spiral or circle pattern when clicked. + + This plugin is useful when you have multiple markers in close proximity that would otherwise be difficult to interact with. + When a user clicks on a cluster of overlapping markers, they spread out in a 'spider' pattern, making each marker + individually accessible. + + Markers must be added to the map **before** calling `oms.add_to(map)`. + The plugin identifies and manages all markers already present on the map. + 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 - + The options to configure the spiderfier behavior: + - keepSpiderfied : bool, default True + If true, markers stay spiderfied after clicking + - nearbyDistance : int, default 20 + Pixels away from a marker that is considered overlapping + - legWeight : float, default 1.5 + Weight of the spider legs + - circleSpiralSwitchover : int, optional + Number of markers at which to switch from circle to spiral pattern + Example ------- - >>> markers = [marker1, marker2, marker3] # Create some markers - >>> spiderfier = OverlappingMarkerSpiderfier( - ... markers=markers, keepSpiderfied=True, nearbyDistance=20 - ... ) - >>> spiderfier.add_to(m) # Add to your map + >>> oms = OverlappingMarkerSpiderfier(options={ + ... "keepSpiderfied": True, + ... "nearbyDistance": 30, + ... "legWeight": 2.0 + ... }) + >>> oms.add_to(map) """ - _template = Template( """ {% macro script(this, kwargs) %} - var {{ this.get_name() }} = (function () { - var layerGroup = L.layerGroup(); - + (function () { try { var oms = new OverlappingMarkerSpiderfier( {{ this._parent.get_name() }}, {{ this.options|tojson }} ); - var popup = L.popup({ - offset: L.point(0, -30) + oms.addListener('spiderfy', function() { + {{ this._parent.get_name() }}.closePopup(); }); - 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); + {{ this._parent.get_name() }}.eachLayer(function(layer) { + if ( + layer instanceof L.Marker + ) { + oms.addMarker(layer); } }); - 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); + console.error('Error initializing OverlappingMarkerSpiderfier:', error); } - - return layerGroup; })(); {% endmacro %} - """ ) @@ -105,21 +72,9 @@ class OverlappingMarkerSpiderfier(JSCSSMixin, Layer): ) ] - 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) + def __init__(self, options=None, **kwargs): + super().__init__() self._name = "OverlappingMarkerSpiderfier" - - self.markers = markers or [] - default_options = { "keepSpiderfied": True, "nearbyDistance": 20, @@ -127,5 +82,4 @@ def __init__( } if options: default_options.update(options) - self.options = parse_options(**default_options, **kwargs) diff --git a/tests/plugins/test_overlapping_marker_spiderfier.py b/tests/plugins/test_overlapping_marker_spiderfier.py index 204ba365de..87186f6474 100644 --- a/tests/plugins/test_overlapping_marker_spiderfier.py +++ b/tests/plugins/test_overlapping_marker_spiderfier.py @@ -4,14 +4,30 @@ """ import numpy as np -from jinja2 import Template -import folium -from folium import plugins -from folium.utilities import normalize +from folium.folium import Map +from folium.map import Marker +from folium.plugins.overlapping_marker_spiderfier import OverlappingMarkerSpiderfier -def test_overlapping_marker_spiderfier(): +def test_oms_js_inclusion(): + """ + Test that the OverlappingMarkerSpiderfier JavaScript library is included in the map. + """ + m = Map([45.05, 3.05], zoom_start=14) + oms = OverlappingMarkerSpiderfier().add_to(m) + + rendered_map = m._parent.render() + assert ( + '' + in rendered_map + ), "OverlappingMarkerSpiderfier JS file is missing in the rendered output." + + +def test_marker_addition(): + """ + Test that markers are correctly added to the map. + """ N = 10 np.random.seed(seed=26082009) data = np.array( @@ -21,84 +37,38 @@ def test_overlapping_marker_spiderfier(): ] ).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) - ] + m = Map([45.05, 3.05], zoom_start=14) + markers = [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; - })(); - """ + assert len(m._children) == len(markers) + 1, ( + f"Expected {len(markers)} markers on the map, but found { + len(m._children) - 1}." ) - expected = normalize(tmpl_for_expected.render(this=oms)) - out = normalize(m._parent.render()) - assert ( - '' - in out - ) +def test_map_bounds(): + """ + Test that the map bounds correctly encompass all added markers. + """ + 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 - assert expected in out + m = Map([45.05, 3.05], zoom_start=14) + markers = [Marker(location=loc, popup=f"Marker { + i}") for i, loc in enumerate(data)] + + for marker in markers: + marker.add_to(m) bounds = m.get_bounds() assert bounds is not None, "Map bounds should not be None" @@ -106,7 +76,21 @@ def test_overlapping_marker_spiderfier(): 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 + assert bounds[0][0] <= min_lat, "Map bounds do not correctly include the minimum latitude." + assert bounds[0][1] <= min_lon, "Map bounds do not correctly include the minimum longitude." + assert bounds[1][0] >= max_lat, "Map bounds do not correctly include the maximum latitude." + assert bounds[1][1] >= max_lon, "Map bounds do not correctly include the maximum longitude." + + +def test_overlapping_marker_spiderfier_integration(): + """ + Test that OverlappingMarkerSpiderfier integrates correctly with the map. + """ + m = Map([45.05, 3.05], zoom_start=14) + oms = OverlappingMarkerSpiderfier( + options={"keepSpiderfied": True, "nearbyDistance": 20}) + oms.add_to(m) + + assert oms.get_name() in m._children, ( + f"OverlappingMarkerSpiderfier is not correctly added to the map." + ) From 07e536d24267af312c276890325aa12feefe7099 Mon Sep 17 00:00:00 2001 From: swtormy Date: Mon, 18 Nov 2024 10:27:23 +0300 Subject: [PATCH 05/10] fix: resolve pre-commit issues --- .../plugins/overlapping_marker_spiderfier.md | 1 - .../plugins/overlapping_marker_spiderfier.py | 21 ++++---- .../test_overlapping_marker_spiderfier.py | 52 +++++++++++++------ 3 files changed, 46 insertions(+), 28 deletions(-) diff --git a/docs/user_guide/plugins/overlapping_marker_spiderfier.md b/docs/user_guide/plugins/overlapping_marker_spiderfier.md index 8895bdc823..9ab4f46e19 100644 --- a/docs/user_guide/plugins/overlapping_marker_spiderfier.md +++ b/docs/user_guide/plugins/overlapping_marker_spiderfier.md @@ -27,4 +27,3 @@ oms.add_to(m) m ``` - diff --git a/folium/plugins/overlapping_marker_spiderfier.py b/folium/plugins/overlapping_marker_spiderfier.py index b3c5bbda57..b0aa260770 100644 --- a/folium/plugins/overlapping_marker_spiderfier.py +++ b/folium/plugins/overlapping_marker_spiderfier.py @@ -1,18 +1,20 @@ from jinja2 import Template + from folium.elements import JSCSSMixin, MacroElement from folium.utilities import parse_options + class OverlappingMarkerSpiderfier(JSCSSMixin, MacroElement): """ A plugin that handles overlapping markers on a map by spreading them out in a spiral or circle pattern when clicked. - + This plugin is useful when you have multiple markers in close proximity that would otherwise be difficult to interact with. - When a user clicks on a cluster of overlapping markers, they spread out in a 'spider' pattern, making each marker + When a user clicks on a cluster of overlapping markers, they spread out in a 'spider' pattern, making each marker individually accessible. - Markers must be added to the map **before** calling `oms.add_to(map)`. + Markers must be added to the map **before** calling `oms.add_to(map)`. The plugin identifies and manages all markers already present on the map. - + Parameters ---------- options : dict, optional @@ -25,16 +27,15 @@ class OverlappingMarkerSpiderfier(JSCSSMixin, MacroElement): Weight of the spider legs - circleSpiralSwitchover : int, optional Number of markers at which to switch from circle to spiral pattern - + Example ------- - >>> oms = OverlappingMarkerSpiderfier(options={ - ... "keepSpiderfied": True, - ... "nearbyDistance": 30, - ... "legWeight": 2.0 - ... }) + >>> oms = OverlappingMarkerSpiderfier( + ... options={"keepSpiderfied": True, "nearbyDistance": 30, "legWeight": 2.0} + ... ) >>> oms.add_to(map) """ + _template = Template( """ {% macro script(this, kwargs) %} diff --git a/tests/plugins/test_overlapping_marker_spiderfier.py b/tests/plugins/test_overlapping_marker_spiderfier.py index 87186f6474..4828e2a8b5 100644 --- a/tests/plugins/test_overlapping_marker_spiderfier.py +++ b/tests/plugins/test_overlapping_marker_spiderfier.py @@ -15,7 +15,7 @@ def test_oms_js_inclusion(): Test that the OverlappingMarkerSpiderfier JavaScript library is included in the map. """ m = Map([45.05, 3.05], zoom_start=14) - oms = OverlappingMarkerSpiderfier().add_to(m) + OverlappingMarkerSpiderfier().add_to(m) rendered_map = m._parent.render() assert ( @@ -38,16 +38,20 @@ def test_marker_addition(): ).T m = Map([45.05, 3.05], zoom_start=14) - markers = [Marker(location=loc, popup=f"Marker { - i}") for i, loc in enumerate(data)] + markers = [ + Marker( + location=loc, + popup=f"Marker {i}", + ) + for i, loc in enumerate(data) + ] for marker in markers: marker.add_to(m) - assert len(m._children) == len(markers) + 1, ( - f"Expected {len(markers)} markers on the map, but found { - len(m._children) - 1}." - ) + assert ( + len(m._children) == len(markers) + 1 + ), f"Expected {len(markers)} markers, found {len(m._children) - 1}." def test_map_bounds(): @@ -64,8 +68,13 @@ def test_map_bounds(): ).T m = Map([45.05, 3.05], zoom_start=14) - markers = [Marker(location=loc, popup=f"Marker { - i}") for i, loc in enumerate(data)] + markers = [ + Marker( + location=loc, + popup=f"Marker {i}", + ) + for i, loc in enumerate(data) + ] for marker in markers: marker.add_to(m) @@ -76,10 +85,18 @@ def test_map_bounds(): min_lat, min_lon = data.min(axis=0) max_lat, max_lon = data.max(axis=0) - assert bounds[0][0] <= min_lat, "Map bounds do not correctly include the minimum latitude." - assert bounds[0][1] <= min_lon, "Map bounds do not correctly include the minimum longitude." - assert bounds[1][0] >= max_lat, "Map bounds do not correctly include the maximum latitude." - assert bounds[1][1] >= max_lon, "Map bounds do not correctly include the maximum longitude." + assert ( + bounds[0][0] <= min_lat + ), "Map bounds do not correctly include the minimum latitude." + assert ( + bounds[0][1] <= min_lon + ), "Map bounds do not correctly include the minimum longitude." + assert ( + bounds[1][0] >= max_lat + ), "Map bounds do not correctly include the maximum latitude." + assert ( + bounds[1][1] >= max_lon + ), "Map bounds do not correctly include the maximum longitude." def test_overlapping_marker_spiderfier_integration(): @@ -88,9 +105,10 @@ def test_overlapping_marker_spiderfier_integration(): """ m = Map([45.05, 3.05], zoom_start=14) oms = OverlappingMarkerSpiderfier( - options={"keepSpiderfied": True, "nearbyDistance": 20}) + options={"keepSpiderfied": True, "nearbyDistance": 20} + ) oms.add_to(m) - assert oms.get_name() in m._children, ( - f"OverlappingMarkerSpiderfier is not correctly added to the map." - ) + assert ( + oms.get_name() in m._children + ), "OverlappingMarkerSpiderfier is not correctly added to the map." From 3ea2a7f87df015ba22d45dc4d9410b86a290f4ba Mon Sep 17 00:00:00 2001 From: Konstantin <58854613+swtormy@users.noreply.github.com> Date: Sat, 23 Nov 2024 22:34:06 +0300 Subject: [PATCH 06/10] Update folium/plugins/overlapping_marker_spiderfier.py Co-authored-by: Frank Anema <33519926+Conengmo@users.noreply.github.com> --- folium/plugins/overlapping_marker_spiderfier.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/folium/plugins/overlapping_marker_spiderfier.py b/folium/plugins/overlapping_marker_spiderfier.py index b0aa260770..d925a8b121 100644 --- a/folium/plugins/overlapping_marker_spiderfier.py +++ b/folium/plugins/overlapping_marker_spiderfier.py @@ -73,7 +73,14 @@ class OverlappingMarkerSpiderfier(JSCSSMixin, MacroElement): ) ] - def __init__(self, options=None, **kwargs): + def __init__( + self, + keep_spiderfied: bool = True, + nearby_distance: int = 20, + leg_weight: float = 1.5, + circle_spiral_switchover: int = 9, + **kwargs + ): super().__init__() self._name = "OverlappingMarkerSpiderfier" default_options = { From d8c60188b39b1e143e2c8d55985636d767318f53 Mon Sep 17 00:00:00 2001 From: Konstantin <58854613+swtormy@users.noreply.github.com> Date: Sat, 23 Nov 2024 22:40:21 +0300 Subject: [PATCH 07/10] Update folium/plugins/overlapping_marker_spiderfier.py Co-authored-by: Frank Anema <33519926+Conengmo@users.noreply.github.com> --- folium/plugins/overlapping_marker_spiderfier.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/folium/plugins/overlapping_marker_spiderfier.py b/folium/plugins/overlapping_marker_spiderfier.py index d925a8b121..812c5aa834 100644 --- a/folium/plugins/overlapping_marker_spiderfier.py +++ b/folium/plugins/overlapping_marker_spiderfier.py @@ -50,13 +50,9 @@ class OverlappingMarkerSpiderfier(JSCSSMixin, MacroElement): {{ this._parent.get_name() }}.closePopup(); }); - {{ this._parent.get_name() }}.eachLayer(function(layer) { - if ( - layer instanceof L.Marker - ) { - oms.addMarker(layer); - } - }); + {%- for marker in this.markers %} + oms.addMarker({{ marker.get_name() }}); + {%- endfor %} } catch (error) { console.error('Error initializing OverlappingMarkerSpiderfier:', error); From 42eefa2a4347006a3064832d49a226f361608624 Mon Sep 17 00:00:00 2001 From: swtormy Date: Sun, 24 Nov 2024 12:37:05 +0300 Subject: [PATCH 08/10] feat: add support for spiderifying markers in FeatureGroups --- .../plugins/overlapping_marker_spiderfier.md | 90 +++++++++++++++++-- .../plugins/overlapping_marker_spiderfier.py | 73 +++++++++------ .../test_overlapping_marker_spiderfier.py | 3 +- 3 files changed, 130 insertions(+), 36 deletions(-) diff --git a/docs/user_guide/plugins/overlapping_marker_spiderfier.md b/docs/user_guide/plugins/overlapping_marker_spiderfier.md index 9ab4f46e19..bff8c11596 100644 --- a/docs/user_guide/plugins/overlapping_marker_spiderfier.md +++ b/docs/user_guide/plugins/overlapping_marker_spiderfier.md @@ -2,6 +2,8 @@ The `OverlappingMarkerSpiderfier` is a plugin for Folium that helps manage overlapping markers by "spiderfying" them when clicked, making it easier to select individual markers. +## Using with Markers + ```{code-cell} ipython3 import folium from folium.plugins import OverlappingMarkerSpiderfier @@ -17,13 +19,89 @@ for i in range(20): ).add_to(m) # Add the OverlappingMarkerSpiderfier plugin -oms = OverlappingMarkerSpiderfier(options={ - "keepSpiderfied": True, # Markers remain spiderfied after clicking - "nearbyDistance": 20, # Distance for clustering markers in pixel - "circleSpiralSwitchover": 10, # Threshold for switching between circle and spiral - "legWeight": 2.0 # Line thickness for spider legs -}) +oms = OverlappingMarkerSpiderfier( + keep_spiderfied=True, # Markers remain spiderfied after clicking + nearby_distance=20, # Distance for clustering markers in pixel + circle_spiral_switchover=10, # Threshold for switching between circle and spiral + leg_weight=2.0 # Line thickness for spider legs + ) oms.add_to(m) m ``` + +## Using with FeatureGroups + +```{code-cell} ipython3 +import folium +from folium.plugins import OverlappingMarkerSpiderfier + +# Create a map +m = folium.Map(location=[45.05, 3.05], zoom_start=13) + +# Create a FeatureGroup +feature_group = folium.FeatureGroup(name='Feature Group') + +# Add markers to the FeatureGroup +for i in range(10): + folium.Marker( + location=[45.05 + i * 0.0001, 3.05 + i * 0.0001], + popup=f"Feature Group Marker {i}" + ).add_to(feature_group) + +# Add the FeatureGroup to the map +feature_group.add_to(m) + +# Initialize OverlappingMarkerSpiderfier +oms = OverlappingMarkerSpiderfier() +oms.add_to(m) + +m +``` + +## Using with FeatureGroupSubGroups + +```{code-cell} ipython3 +import folium +from folium.plugins import OverlappingMarkerSpiderfier, FeatureGroupSubGroup + +# Create a map +m = folium.Map(location=[45.05, 3.05], zoom_start=13) + +# Create a main FeatureGroup +main_group = folium.FeatureGroup(name='Main Group') + +# Create sub-groups +sub_group1 = FeatureGroupSubGroup(main_group, name='Sub Group 1') +sub_group2 = FeatureGroupSubGroup(main_group, name='Sub Group 2') + +# Add markers to the first sub-group +for i in range(10): + folium.Marker( + location=[45.05 + i * 0.0001, 3.05 + i * 0.0001], + popup=f"Sub Group 1 Marker {i}" + ).add_to(sub_group1) + +# Add markers to the second sub-group +for i in range(10, 20): + folium.Marker( + location=[45.06 + (i - 10) * 0.0001, 3.06 + (i - 10) * 0.0001], + popup=f"Sub Group 2 Marker {i}" + ).add_to(sub_group2) + +# Add sub-groups to the map +sub_group1.add_to(m) +sub_group2.add_to(m) + +# Add the main group to the map +main_group.add_to(m) + +# Initialize OverlappingMarkerSpiderfier +oms = OverlappingMarkerSpiderfier() +oms.add_to(m) + +# Add the LayerControl plugin +folium.LayerControl().add_to(m) + +m +``` diff --git a/folium/plugins/overlapping_marker_spiderfier.py b/folium/plugins/overlapping_marker_spiderfier.py index 812c5aa834..d69aa5ae6e 100644 --- a/folium/plugins/overlapping_marker_spiderfier.py +++ b/folium/plugins/overlapping_marker_spiderfier.py @@ -1,6 +1,9 @@ +from typing import Optional + from jinja2 import Template -from folium.elements import JSCSSMixin, MacroElement +from folium.elements import Element, JSCSSMixin, MacroElement +from folium.map import Marker from folium.utilities import parse_options @@ -12,26 +15,24 @@ class OverlappingMarkerSpiderfier(JSCSSMixin, MacroElement): When a user clicks on a cluster of overlapping markers, they spread out in a 'spider' pattern, making each marker individually accessible. - Markers must be added to the map **before** calling `oms.add_to(map)`. - The plugin identifies and manages all markers already present on the map. + Markers are automatically identified and managed by the plugin, so there is no need to add them separately. + Simply add the plugin to the map using `oms.add_to(map)`. Parameters ---------- - options : dict, optional - The options to configure the spiderfier behavior: - - keepSpiderfied : bool, default True - If true, markers stay spiderfied after clicking - - nearbyDistance : int, default 20 - Pixels away from a marker that is considered overlapping - - legWeight : float, default 1.5 - Weight of the spider legs - - circleSpiralSwitchover : int, optional - Number of markers at which to switch from circle to spiral pattern + keep_spiderfied : bool, default True + If true, markers stay spiderfied after clicking. + nearby_distance : int, default 20 + Pixels away from a marker that is considered overlapping. + leg_weight : float, default 1.5 + Weight of the spider legs. + circle_spiral_switchover : int, optional + Number of markers at which to switch from circle to spiral pattern. Example ------- >>> oms = OverlappingMarkerSpiderfier( - ... options={"keepSpiderfied": True, "nearbyDistance": 30, "legWeight": 2.0} + ... keep_spiderfied=True, nearby_distance=30, leg_weight=2.0 ... ) >>> oms.add_to(map) """ @@ -53,7 +54,6 @@ class OverlappingMarkerSpiderfier(JSCSSMixin, MacroElement): {%- for marker in this.markers %} oms.addMarker({{ marker.get_name() }}); {%- endfor %} - } catch (error) { console.error('Error initializing OverlappingMarkerSpiderfier:', error); } @@ -70,20 +70,35 @@ class OverlappingMarkerSpiderfier(JSCSSMixin, MacroElement): ] def __init__( - self, - keep_spiderfied: bool = True, - nearby_distance: int = 20, - leg_weight: float = 1.5, - circle_spiral_switchover: int = 9, - **kwargs + self, + keep_spiderfied: bool = True, + nearby_distance: int = 20, + leg_weight: float = 1.5, + circle_spiral_switchover: int = 9, + **kwargs ): super().__init__() self._name = "OverlappingMarkerSpiderfier" - default_options = { - "keepSpiderfied": True, - "nearbyDistance": 20, - "legWeight": 1.5, - } - if options: - default_options.update(options) - self.options = parse_options(**default_options, **kwargs) + self.options = parse_options( + keep_spiderfied=keep_spiderfied, + nearby_distance=nearby_distance, + leg_weight=leg_weight, + circle_spiral_switchover=circle_spiral_switchover, + **kwargs + ) + + def add_to( + self, parent: Element, name: Optional[str] = None, index: Optional[int] = None + ) -> Element: + self._parent = parent + self.markers = self._get_all_markers(parent) + super().add_to(parent, name=name, index=index) + + def _get_all_markers(self, element: Element) -> list: + markers = [] + for child in element._children.values(): + if isinstance(child, Marker): + markers.append(child) + elif hasattr(child, "_children"): + markers.extend(self._get_all_markers(child)) + return markers diff --git a/tests/plugins/test_overlapping_marker_spiderfier.py b/tests/plugins/test_overlapping_marker_spiderfier.py index 4828e2a8b5..45004d5383 100644 --- a/tests/plugins/test_overlapping_marker_spiderfier.py +++ b/tests/plugins/test_overlapping_marker_spiderfier.py @@ -105,7 +105,8 @@ def test_overlapping_marker_spiderfier_integration(): """ m = Map([45.05, 3.05], zoom_start=14) oms = OverlappingMarkerSpiderfier( - options={"keepSpiderfied": True, "nearbyDistance": 20} + keep_spiderfied=True, + nearby_distance=20, ) oms.add_to(m) From eb898c20724138786ee11ac7d8510842fa8879fa Mon Sep 17 00:00:00 2001 From: swtormy Date: Sun, 24 Nov 2024 13:55:01 +0300 Subject: [PATCH 09/10] docs: modification of OverlappingMarkerSpiderfier plugin documentation --- docs/user_guide/plugins/overlapping_marker_spiderfier.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/user_guide/plugins/overlapping_marker_spiderfier.md b/docs/user_guide/plugins/overlapping_marker_spiderfier.md index bff8c11596..957f841fba 100644 --- a/docs/user_guide/plugins/overlapping_marker_spiderfier.md +++ b/docs/user_guide/plugins/overlapping_marker_spiderfier.md @@ -89,13 +89,13 @@ for i in range(10, 20): popup=f"Sub Group 2 Marker {i}" ).add_to(sub_group2) +# Add the main group to the map +main_group.add_to(m) + # Add sub-groups to the map sub_group1.add_to(m) sub_group2.add_to(m) -# Add the main group to the map -main_group.add_to(m) - # Initialize OverlappingMarkerSpiderfier oms = OverlappingMarkerSpiderfier() oms.add_to(m) From 6ec0e0905c639442e266fda51b42bddd414cf341 Mon Sep 17 00:00:00 2001 From: Frank Anema <33519926+Conengmo@users.noreply.github.com> Date: Tue, 26 Nov 2024 09:39:13 +0100 Subject: [PATCH 10/10] Update folium/plugins/overlapping_marker_spiderfier.py --- folium/plugins/overlapping_marker_spiderfier.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/folium/plugins/overlapping_marker_spiderfier.py b/folium/plugins/overlapping_marker_spiderfier.py index d69aa5ae6e..70fcea4129 100644 --- a/folium/plugins/overlapping_marker_spiderfier.py +++ b/folium/plugins/overlapping_marker_spiderfier.py @@ -26,7 +26,7 @@ class OverlappingMarkerSpiderfier(JSCSSMixin, MacroElement): Pixels away from a marker that is considered overlapping. leg_weight : float, default 1.5 Weight of the spider legs. - circle_spiral_switchover : int, optional + circle_spiral_switchover : int, default 9 Number of markers at which to switch from circle to spiral pattern. Example