Folium: Choropleths, tooltips/popups and pandas dataframes

Created on 26 Aug 2020  路  6Comments  路  Source: python-visualization/folium

Is your feature request related to a problem? Please describe.
Sorry if this has been resolved already or if I'm missing something obvious. I'm pretty new to Folium and this is my first time posting a question/request on github. I think I have seen this issue mentioned before but I'm uncertain if solutions where implemented or rejected.

I have been trying to wrap my head around how to create polygon specific popups or tooltips when creating choropleth-map. I would like to display data concerning the specific region from the dataframe in a popup or tooltip when clicking or hovering the pointer over the region on the map.

As far I understand the documentation it is only possible to display information that's in the GeoJson using folium.features.GeoJsonTooltip. The problem with that solution is that you need to merge the data from the pandas dataframe into the GeoJson, which goes against the convenience handling the data in pandas and the geographical data in GeoJson.

I have tried using both a folium.Choropleth object and making my own using folium.GeoJson.

Is there some example of how this could be achieved using Folium and regular pandas?

Describe the solution you'd like
I would like to be able to code something like this to access data from the pandas dataframe:

m = folium.Map(location=[lat, long], width=600, height=1000, zoom_start=5)

choro = folium.Choropleth(
        geo_data=GeoJsonshape,
        name='Unemployment',
        data=dataframe,
        columns=['region', 'rate'],
        key_on='feature.properties.name',
        legend_name='Unemployment'
        tooltip=dataframe['rate'].where([feature['properties']['name']]
        )

choro.add_to(m)

or

choro = folium.GeoJson(
        GeoJsonshape,
        name='Unemployment',,
        tooltip=dataframe['rate'].where([feature['properties']['name']],
        style_function=lambda feature: {
        'fillColor': colormap(data[feature['properties']['name']]),
        'color': 'black',
        'weight': 1,
        'fillOpacity': 0.6,
        'line_opacity': 0.2,
    }
    )

Describe alternatives you've considered
Another solution I think would be to use Geopandas to join the GeoJson with the dataframe, but this is not practically feasible solution for me. Geopandas is dependent on GDAL. My current environment makes it more or less impossible to install this.

If anyone can give me any pointers on how to solve this I would be grateful. I have tried to look at the folium code to perhaps implement a work around for myself but it is above my current ability.

Most helpful comment

Describe alternatives you've considered
Another solution I think would be to use Geopandas to join the GeoJson with the Dataframe, but this is not practically feasible solution for me. Geopandas is dependent on GDAL. My current environment makes it more or less impossible to install this.

@ostropunk the core issue is that the fields you want to show in the tooltip need to be available in the geojson properties. If you can't do use geopandas to do the join, one option would be to do it iteratively in python by looping through each of the features and finding the corresponding data point you want to put in to the tooltip. Then you should be able to add the keys you want to display to the GeoJsonTooltip.fields list.

Maybe something like:

import json
import pandas as pd

def merge(geojson_path: str, df: pd.DataFrame, geo_key: str = "id", df_key: str = "id"):
    with open(geojson_path, 'r') as file:
        __geo_interface__ = json.load(file)

    records = json.loads(df.to_json(orient="records"))

    for feature in __geo_interface__.get('features'):
        id = feature['properties'].get(geo_key)
        for record in records:
            if record.get(df_key) == id:
                feature['properties'].update(**record)
    return __geo_interface__

write out the returned object from merge to a json file, and pass that path to the geo_data property of Choropleth

I am not at all familiar with jQuery, which is what some of this looks like, but I did work out that the lookup of the value that is being displayed in the tooltip comes from this bit:

@markhudson42 the $ syntax in tick marks is an ES6 method for string interpolation.
For example:

const x = "test"
const y = `${x}`
console.log(x === y) // true

It's helpful for interpolating special or escape characters that might otherwise break string concatenation methods, which has posed some issues in the past.

All 6 comments

Hello,

I am quite a beginner with javascript and the underlying folium code, too, but what I have been doing lately when trying to understand what folium is doing behind the scenes is to create an html file of an example that I want to investigate and maybe extend, and then I play around with the html and work through things in the browser javascript debugger and see what I can work out.

For your example, I don't think it can be done using existing functionality but I started with this example:

m = folium.Map(location=[48, -102], zoom_start=3)

choro = folium.GeoJson(
            state_geo,
            name='Unemployment',
            style_function=lambda feature: {
            'fillOpacity': 0.5,
            'weight': 1,
            'fillColor': 'black',
            'color': 'black'
            }
        )

choro.add_to(m)

# add geojsontooltip?
gjs_tooltip = folium.GeoJsonTooltip( 
    fields=['name'], 
    aliases=['State'], 
    localize=True, 
    style=('background-color: grey; color: white; font-family:' 
           'courier new; font-size: 24px; padding: 10px;') 
)

gjs_tooltip.add_to(choro)

folium.LayerControl().add_to(m)

m.save("geojson_tooltip_example.html")
m

and looked at what it is in the corresponding javascript that is used to extract the fields from the data that is passed to the folium.GeoJson function so that it can be used by the folium.GeoJsonTooltip function.

In my HTML, I have the following:

    geo_json_938c91e9ef9041a8872f48536c232b94.bindTooltip(
    function(layer){
    let div = L.DomUtil.create('div');

    let handleObject = feature=>typeof(feature)=='object' ? JSON.stringify(feature) : feature;
    let fields = ["name"];
    let aliases = ["State"];
    let table = '<table>' + String(fields.map((v,i)=> `<tr><th>${aliases[i].toLocaleString()}</th><td>${handleObject(layer.feature.properties[v]).toLocaleString()}</td></tr>`).join('')) +'</table>';

    div.innerHTML=table;

    return div
    }
    ,{"className": "foliumtooltip", "sticky": true});

I am not at all familiar with jQuery, which is what some of this looks like, but I did work out that the lookup of the value that is being displayed in the tooltip comes from this bit:

${handleObject(layer.feature.properties[v]).toLocaleString()

I don't know what the map function is doing, but v contains "name", and layer.feature,properties[v] returns the value of "name" for the current feature under the mouse pointer, which could be, say, "Alaska".

So, as an experiment I created a dummy variable in the javascript section of the html and defined a random number for each state:

let dummy = {"Alabama": 0.127147429, "Alaska": 0.951358548, "Arizona": 0.405535878, ...

and then modified the code involving $handleObject so that it looks up in this dummy variable:

let table = '<table>' + String(fields.map((v,i)=> `<tr><th>${aliases[i].toLocaleString()}</th><td>${handleObject(dummy[layer.feature.properties[v]]).toLocaleString()}</td></tr>`).join('')) +'</table>';

and this works:

image

So... I am presuming that someone with much more knowledge of writing good javascript could probably work out how to do something similar to this by modifying the javascript macro. I think that what would be needed is a way to define various data objects storing the output values for each input field value, and then use that object as a lookup on the original feature property.

I think doing this for multiple fields would be more complicated than in my simple hack, but it looks possible.

Mark.

Describe alternatives you've considered
Another solution I think would be to use Geopandas to join the GeoJson with the Dataframe, but this is not practically feasible solution for me. Geopandas is dependent on GDAL. My current environment makes it more or less impossible to install this.

@ostropunk the core issue is that the fields you want to show in the tooltip need to be available in the geojson properties. If you can't do use geopandas to do the join, one option would be to do it iteratively in python by looping through each of the features and finding the corresponding data point you want to put in to the tooltip. Then you should be able to add the keys you want to display to the GeoJsonTooltip.fields list.

Maybe something like:

import json
import pandas as pd

def merge(geojson_path: str, df: pd.DataFrame, geo_key: str = "id", df_key: str = "id"):
    with open(geojson_path, 'r') as file:
        __geo_interface__ = json.load(file)

    records = json.loads(df.to_json(orient="records"))

    for feature in __geo_interface__.get('features'):
        id = feature['properties'].get(geo_key)
        for record in records:
            if record.get(df_key) == id:
                feature['properties'].update(**record)
    return __geo_interface__

write out the returned object from merge to a json file, and pass that path to the geo_data property of Choropleth

I am not at all familiar with jQuery, which is what some of this looks like, but I did work out that the lookup of the value that is being displayed in the tooltip comes from this bit:

@markhudson42 the $ syntax in tick marks is an ES6 method for string interpolation.
For example:

const x = "test"
const y = `${x}`
console.log(x === y) // true

It's helpful for interpolating special or escape characters that might otherwise break string concatenation methods, which has posed some issues in the past.

@markhudson42 Nice, but way over my head. I really hope more people with better coding skills than I would find this useful. Thanks for taking your time!

@jtbaker Thanks for the pointers, and your time! I poured over this a bit more today and I ended up with a hack pretty similar to your suggestion. It worked!

datalist = dataframe[column].to_list()

with open(geodata) as f:
    geojson = json.load(f)

for index, item in enumerate(geojson['features']):
    item['properties'].update({'data':datalist[index]})

@jtbaker Thanks for the pointers, and your time! I poured over this a bit more today and I ended up with a hack pretty similar to your suggestion. It worked!

Sounds good! With this position-based methodology, you just need to make sure that the ordering for your data sources for the geo-data and your dataframe values match up, and that they are 1:1 with each other, otherwise you may be inadvertently adding the data attributes of different features to your geo-data.

Looks like I was trying to be far too complicated again, but I'm glad someone with more knowledge has shown the way!

Unless there's something to change in folium, I think there's no action needed. Feel free to continue any discussion. We can reopen if needed.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Demetrio92 picture Demetrio92  路  18Comments

achourasia picture achourasia  路  19Comments

ElmWer picture ElmWer  路  24Comments

themiurgo picture themiurgo  路  19Comments

ispmarin picture ispmarin  路  17Comments