python-visualization / folium

Python Data. Leaflet.js Maps.
https://python-visualization.github.io/folium/
MIT License
6.92k stars 2.23k forks source link

Crossing layers in LayerControl() with 2 variables (addition filter) #1331

Closed juancalvof closed 1 year ago

juancalvof commented 4 years ago

Is your feature request related to a problem? Please describe. Not being able to cross layers with 2 variables

Describe the solution you'd like I use FeatureGroup() for creating groups/layers for the LayerControl(). For example, if I have 3 years (2018,2019,2020), in a variable call YEAR, I will have, 3 FeatureGroups. Until here, everything ok.

My request, my problem, comes in how to add another variable, with 3 options (example:type_a,type_b,type_c, variable name TYPE). I know how to make FeatureGroupSubGroup inside each year, but that will give me 12 layers. I want to have 6 layers, for filtering, and where each layer gives control of each value in the 2 variables. This means, that at minimum, we need to have a check of 1 layer of YEAR and 1 layer of TYPE, to see something.

Describe alternatives you've considered None

Additional context Example of what I want to achieve 20200516_142819

Example code of what I can achieve Screenshot (100)

import folium
import folium.plugins
import pandas as pd

# Create df
data = {"id": ["01", "02", "03", "04", "05", "06"],
        "year": [2018, 2019, 2020, 2019, 2019, 2018],
        "type": ["type_a", "type_a", "type_b", "type_b", "type_c", "type_c"],
        "latitude": [-12.1, -12.2, -12.3, -12.4, -12.5, -12.6],
        "longitude": [-72.1, -72.2, -72.3, -72.4, -72.5, -72.6]}

# Respects order
df = pd.DataFrame(data, columns=["id", "year", "type", "latitude", "longitude"])

def _plot_dot(point, list_groups, list_subgroups_2018, list_subgroups_2019, list_subgroups_2020,
              radius=4, weight=1,color='black'):
    # group_base = folium.FeatureGroup(name="year_Sin year").add_to(map_element)

    groups_dict = {2018: list_groups[0], 2019: list_groups[1], 2020: list_groups[2]}
    subgroups_dict_2018 = {"type_a": list_subgroups_2018[0], "type_b": list_subgroups_2018[1],
                           "type_c": list_subgroups_2018[2]}
    subgroups_dict_2019 = {"type_a": list_subgroups_2019[0], "type_b": list_subgroups_2019[1],
                           "type_c": list_subgroups_2019[2]}
    subgroups_dict_2020 = {"type_a": list_subgroups_2020[0], "type_b": list_subgroups_2020[1],
                           "type_c": list_subgroups_2020[2]}
    place = None
    for k, v in groups_dict.items():
        if point["year"] == k:
            if point["year"] == 2018:
                for y, z in subgroups_dict_2018.items():
                    if point["type"] == y:
                        place = z
            elif point["year"] == 2019:
                for y, z in subgroups_dict_2019.items():
                    if point["type"] == y:
                        place = z
            elif point["year"] == 2020:
                for y, z in subgroups_dict_2020.items():
                    if point["type"] == y:
                        place = z

    folium.CircleMarker(location=[point["latitude"], point["longitude"]], radius=radius, weight=weight,
                        color=color, fill=True,
                        fill_color="red",
                        fill_opacity=0.9,
                        tooltip=f'<b>id: </b>{str(point["id"])}'
                                f'<br></br>'f'<b>year: </b>{str(point["year"])}'
                                f'<br></br>'f'<b>type: </b>{str(point["type"])}'
                        ).add_to(place)

def generate_map(data, filename=None):
    map_element = folium.Map(tiles='cartodbpositron')
    group_2018 = folium.FeatureGroup(name="year_2018").add_to(map_element)
    g1_2018 = folium.plugins.FeatureGroupSubGroup(group_2018, 'type_a_2018').add_to(map_element)
    g2_2018 = folium.plugins.FeatureGroupSubGroup(group_2018, 'type_b_2018').add_to(map_element)
    g3_2018 = folium.plugins.FeatureGroupSubGroup(group_2018, 'type_c_2018').add_to(map_element)
    group_2019 = folium.FeatureGroup(name="year_2019").add_to(map_element)
    g1_2019 = folium.plugins.FeatureGroupSubGroup(group_2019, 'type_a_2019').add_to(map_element)
    g2_2019 = folium.plugins.FeatureGroupSubGroup(group_2019, 'type_b_2019').add_to(map_element)
    g3_2019 = folium.plugins.FeatureGroupSubGroup(group_2019, 'type_c_2019').add_to(map_element)
    group_2020 = folium.FeatureGroup(name="year_2020").add_to(map_element)
    g1_2020 = folium.plugins.FeatureGroupSubGroup(group_2020, 'type_a_2020').add_to(map_element)
    g2_2020 = folium.plugins.FeatureGroupSubGroup(group_2020, 'type_b_2020').add_to(map_element)
    g3_2020 = folium.plugins.FeatureGroupSubGroup(group_2020, 'type_c_2020').add_to(map_element)

    data.apply(_plot_dot, axis=1, args=([group_2018, group_2019, group_2020],
                                        [g1_2018, g2_2018, g3_2018],
                                        [g1_2019, g2_2019, g3_2019],
                                        [g1_2020, g2_2020, g3_2020],
                                        5))
    folium.LayerControl().add_to(map_element)
    map_element.save(filename, close_file=True)

    return map_element

if __name__ == "__main__":
    map_1 = generate_map(df, 'test_maps.html')

Implementation None

Conengmo commented 4 years ago

I think I get what you're trying to do, and indeed this is not supported at the moment. You could look at this list of Leaflet plugins and see if any of these do what you need: https://leafletjs.com/plugins.html#layer-switching-controls. If there is one, you could wrap that plugin in Python. Just look at our other plugins to see how that works. Then, possibly, it could be interesting to open a PR with that. I'm not decided yet on whether we want to include this in folium or not. It could be useful, but if it's too much hassle or has too much overlap with existing plugins we have then we'd better not.

juancalvof commented 4 years ago

Ok. Thanks for your answer. I have checked the layer plugins (switching-control) and don't seem to be any of them that suits my purpose. In a way, what I think would be great, is a way of filtering by more than one categorical variable. I have found this other plugin that does what I want: http://maydemirx.github.io/leaflet-tag-filter-button/ (Check second example with 2 variables)

I will check one plugin of Folium and see if I understand how to wrap it.

It will be great if you could tell me if: 1) Do you consider of interest, adding this plugin? 2) Its something in project of or there have been previous attempts. 3) It's the best way to achieve what I attemp.

Thanks Conegmo for your time.

juancalvof commented 4 years ago

Hi!

I finally founded some time to start this. I have just started making some tests, but I'm having difficulties understanding jinja2 template. I'm a Python developer, not javascript. Any help is welcome :)

from branca.element import CssLink, Figure, JavascriptLink, MacroElement
from jinja2 import Template

from folium.utilities import parse_options

_default_js = [
    ('leaflet-tag-filter-button.js',
     'https://rawcdn.githack.com/maydemirx/leaflet-tag-filter-button/07c7ec8eaa02b8da5838ae112ce8487df4db1f4f/'
     'src/leaflet-tag-filter-button.js')
    ]

_default_css = [
    ('leaflet-tag-filter-button.css',
     'https://rawcdn.githack.com/maydemirx/leaflet-tag-filter-button/07c7ec8eaa02b8da5838ae112ce8487df4db1f4f/src'
     '/leaflet-tag-filter-button.css')
    ]

class TagFilterButton (MacroElement):
    """
    Adds tag filter control for layers (marker, geojson features etc.) to LeafLet.

    Uses the Leaflet plugin by Mehmet Aydemir under MIT license.
    https://github.com/maydemirx/leaflet-tag-filter-button
    """

    _template = Template(u"""
        {% macro script(this, kwargs) %}
            var {{ this.get_name() }} = new L.Control.TagFilterButton(
                {{ this.options|tojson }}
            );
            {{ this._parent.get_name() }}.addControl({{ this.get_name() }});
        {% endmacro %}
    """)

    def __init__(
            self,
            icon='fa-filter',
            onSelectionComplete=None,
            data=None,
            clearText='clear',
            filterOnEveryClick=False,
            openPopupOnHover=False,
            ajaxData=None,
            **kwargs
    ):
        super(TagFilterButton , self).__init__()
        self._name = 'TagFilterButton '

        self.options = parse_options(
            icon=icon,
            onSelectionComeplete=onSelectionComplete,
            data=data,
            clearText=clearText,
            filterOnEveryClick=filterOnEveryClick,
            openPopupOnHover=openPopupOnHover,
            ajaxData=ajaxData,
            **kwargs
        )

    def render(self, **kwargs):
        super(TagFilterButton, self).render(**kwargs)
        figure = self.get_root()
        assert isinstance(figure, Figure), ('You cannot render this Element '
                                            'if it is not in a Figure.')

        for name, url in _default_js:
            figure.header.add_child(JavascriptLink(url), name=name)

        for name, url in _default_css:
            figure.header.add_child(CssLink(url), name=name)
juancalvof commented 4 years ago

I do the call as follow:

tf.TagFilterButton(data=['type_a', 'type_b', 'type_c']).add_to(map_element)

With no positive results. I have doubts about the "data=" argument as list,in Python, which will be an array in Javascript, and the Template of Jinja2. Thanks.

https://ischurov.github.io/pythonvjs/

LINKS OF INTEREST Leaflet plug-in to make example: http://maydemirx.github.io/leaflet-tag-filter-button/

Leaflet plug-in to make code: https://github.com/maydemirx/leaflet-tag-filter-button

jinja2 documentation: https://jinja.palletsprojects.com/en/2.11.x/

gruffoni commented 2 years ago

Has this plugin been integrated in Folium? I am now facing the same kind of problem. Thanks in advance.

Conengmo commented 1 year ago

We merged https://github.com/python-visualization/folium/pull/1343 recently, which will be included in the upcoming 0.14.0 release.