Skip to content

Commit f000f2f

Browse files
jtbakerConengmo
authored andcommitted
GeoJson Popup (#1023)
Added GeoJsonPopup class that extends functionality of GeoJsonTooltip. Shared functionality if moved to GeoJsonDetail parent class.
1 parent 92b5d1b commit f000f2f

File tree

3 files changed

+453
-82
lines changed

3 files changed

+453
-82
lines changed

examples/GeoJsonPopupAndTooltip.ipynb

Lines changed: 279 additions & 0 deletions
Large diffs are not rendered by default.

folium/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
DivIcon,
2626
GeoJson,
2727
GeoJsonTooltip,
28+
GeoJsonPopup,
2829
LatLngPopup,
2930
RegularPolygonMarker,
3031
TopoJson,

folium/features.py

Lines changed: 173 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
none_min,
2626
get_obj_in_upper_tree,
2727
parse_options,
28+
camelize
2829
)
2930
from folium.vector_layers import PolyLine, path_options
3031

@@ -437,7 +438,7 @@ class GeoJson(Layer):
437438

438439
def __init__(self, data, style_function=None, highlight_function=None, # noqa
439440
name=None, overlay=True, control=True, show=True,
440-
smooth_factor=None, tooltip=None, embed=True):
441+
smooth_factor=None, tooltip=None, embed=True, popup=None):
441442
super(GeoJson, self).__init__(name=name, overlay=overlay,
442443
control=control, show=show)
443444
self._name = 'GeoJson'
@@ -467,6 +468,8 @@ def __init__(self, data, style_function=None, highlight_function=None, # noqa
467468
self.add_child(tooltip)
468469
elif tooltip is not None:
469470
self.add_child(Tooltip(tooltip))
471+
if isinstance(popup, (GeoJsonPopup)):
472+
self.add_child(popup)
470473

471474
def process_data(self, data):
472475
"""Convert an unknown data input into a geojson dictionary."""
@@ -796,7 +799,112 @@ def get_bounds(self):
796799
]
797800

798801

799-
class GeoJsonTooltip(Tooltip):
802+
class GeoJsonDetail(MacroElement):
803+
804+
"""
805+
Base class for GeoJsonTooltip and GeoJsonPopup to inherit methods and
806+
template structure from. Not for direct usage.
807+
808+
"""
809+
base_template = u"""
810+
function(layer){
811+
let div = L.DomUtil.create('div');
812+
{% if this.fields %}
813+
let handleObject = feature=>typeof(feature)=='object' ? JSON.stringify(feature) : feature;
814+
let fields = {{ this.fields | tojson | safe }};
815+
let aliases = {{ this.aliases | tojson | safe }};
816+
let table = '<table>' +
817+
String(
818+
fields.map(
819+
(v,i)=>
820+
`<tr>{% if this.labels %}
821+
<th>${aliases[i]{% if this.localize %}.toLocaleString(){% endif %}}</th>
822+
{% endif %}
823+
<td>${handleObject(layer.feature.properties[v]){% if this.localize %}.toLocaleString(){% endif %}}</td>
824+
</tr>`).join(''))
825+
+'</table>';
826+
div.innerHTML=table;
827+
{% endif %}
828+
return div
829+
}
830+
"""
831+
832+
def __init__(self, fields, aliases=None, labels=True, localize=False, style=None,
833+
class_name="geojsondetail"):
834+
super(GeoJsonDetail, self).__init__()
835+
assert isinstance(fields, (list, tuple)), 'Please pass a list or ' \
836+
'tuple to fields.'
837+
if aliases is not None:
838+
assert isinstance(aliases, (list, tuple))
839+
assert len(fields) == len(aliases), 'fields and aliases must have' \
840+
' the same length.'
841+
assert isinstance(labels, bool), 'labels requires a boolean value.'
842+
assert isinstance(localize, bool), 'localize must be bool.'
843+
self._name = "GeoJsonDetail"
844+
self.fields = fields
845+
self.aliases = aliases if aliases is not None else fields
846+
self.labels = labels
847+
self.localize = localize
848+
self.class_name = class_name
849+
if style:
850+
assert isinstance(style, str), \
851+
'Pass a valid inline HTML style property string to style.'
852+
# noqa outside of type checking.
853+
self.style = style
854+
855+
def warn_for_geometry_collections(self):
856+
"""Checks for GeoJson GeometryCollection features to warn user about incompatibility."""
857+
geom_collections = [
858+
feature.get('properties') if feature.get('properties') is not None else key
859+
for key, feature in enumerate(self._parent.data['features'])
860+
if feature['geometry']['type'] == 'GeometryCollection'
861+
]
862+
if any(geom_collections):
863+
warnings.warn(
864+
"{} is not configured to render for GeoJson GeometryCollection geometries. "
865+
"Please consider reworking these features: {} to MultiPolygon for full functionality.\n"
866+
"https://tools.ietf.org/html/rfc7946#page-9".format(self._name, geom_collections), UserWarning)
867+
868+
def render(self, **kwargs):
869+
"""Renders the HTML representation of the element."""
870+
figure = self.get_root()
871+
if isinstance(self._parent, GeoJson):
872+
keys = tuple(self._parent.data['features'][0]['properties'].keys())
873+
self.warn_for_geometry_collections()
874+
elif isinstance(self._parent, TopoJson):
875+
obj_name = self._parent.object_path.split('.')[-1]
876+
keys = tuple(self._parent.data['objects'][obj_name][
877+
'geometries'][0]['properties'].keys())
878+
else:
879+
raise TypeError('You cannot add a {} to anything other than a '
880+
'GeoJson or TopoJson object.'.format(self._name))
881+
keys = tuple(x for x in keys if x not in ('style', 'highlight'))
882+
for value in self.fields:
883+
assert value in keys, ('The field {} is not available in the data. '
884+
'Choose from: {}.'.format(value, keys))
885+
figure.header.add_child(Element(
886+
Template(u"""
887+
<style>
888+
.{{ this.class_name }} {
889+
{{ this.style }}
890+
}
891+
.{{ this.class_name }} table{
892+
margin: auto;
893+
}
894+
.{{ this.class_name }} tr{
895+
text-align: left;
896+
}
897+
.{{ this.class_name }} th{
898+
padding: 2px; padding-right: 8px;
899+
}
900+
</style>
901+
""").render(this=self)), name=self.get_name() + "tablestyle"
902+
)
903+
904+
super(GeoJsonDetail, self).render()
905+
906+
907+
class GeoJsonTooltip(GeoJsonDetail):
800908
"""
801909
Create a tooltip that uses data from either geojson or topojson.
802910
@@ -839,93 +947,76 @@ class GeoJsonTooltip(Tooltip):
839947
>>> GeoJsonTooltip(fields=('CNTY_NM',), labels=False, sticky=False)
840948
"""
841949
_template = Template(u"""
842-
{% macro script(this, kwargs) %}
843-
{{ this._parent.get_name() }}.bindTooltip(
844-
function(layer){
845-
// Convert non-primitive to String.
846-
let handleObject = (feature)=>typeof(feature)=='object' ? JSON.stringify(feature) : feature;
847-
let fields = {{ this.fields|tojson }};
848-
{%- if this.aliases %}
849-
let aliases = {{ this.aliases|tojson }};
850-
{%- endif %}
851-
return '<table{% if this.style %} style={{ this.style|tojson }}{% endif%}>' +
852-
String(
853-
fields.map(
854-
columnname=>
855-
`<tr style="text-align: left;">{% if this.labels %}
856-
<th style="padding: 4px; padding-right: 10px;">{% if this.aliases %}
857-
${aliases[fields.indexOf(columnname)]
858-
{% if this.localize %}.toLocaleString(){% endif %}}
859-
{% else %}
860-
${ columnname{% if this.localize %}.toLocaleString(){% endif %}}
861-
{% endif %}</th>
862-
{% endif %}
863-
<td style="padding: 4px;">${handleObject(layer.feature.properties[columnname])
864-
{% if this.localize %}.toLocaleString(){% endif %}}</td></tr>`
865-
).join(''))
866-
+'</table>'
867-
}, {{ this.options|tojson }});
868-
{% endmacro %}
869-
""")
870-
871-
def __init__(self, fields, aliases=None, labels=True,
872-
localize=False, style=None, sticky=True, **kwargs):
950+
{% macro script(this, kwargs) %}
951+
{{ this._parent.get_name() }}.bindTooltip(""" + GeoJsonDetail.base_template +
952+
u""",{{ this.tooltip_options | tojson | safe }});
953+
{% endmacro %}
954+
""")
955+
956+
def __init__(self, fields, aliases=None, labels=True, localize=False,
957+
style=None, class_name='foliumtooltip', sticky=True, **kwargs):
873958
super(GeoJsonTooltip, self).__init__(
874-
text='', style=style, sticky=sticky, **kwargs
959+
fields=fields, aliases=aliases, labels=labels, localize=localize,
960+
style=style, class_name=class_name
875961
)
876962
self._name = 'GeoJsonTooltip'
963+
kwargs.update({'sticky': sticky, 'class_name': class_name})
964+
self.tooltip_options = {
965+
camelize(key): kwargs[key] for key in kwargs.keys()}
877966

878-
assert isinstance(fields, (list, tuple)), 'Please pass a list or ' \
879-
'tuple to fields.'
880-
if aliases is not None:
881-
assert isinstance(aliases, (list, tuple))
882-
assert len(fields) == len(aliases), 'fields and aliases must have' \
883-
' the same length.'
884-
assert isinstance(labels, bool), 'labels requires a boolean value.'
885-
assert isinstance(localize, bool), 'localize must be bool.'
886-
assert 'permanent' not in kwargs, 'The `permanent` option does not ' \
887-
'work with GeoJsonTooltip.'
888967

889-
self.fields = fields
890-
self.aliases = aliases
891-
self.labels = labels
892-
self.localize = localize
893-
if style:
894-
assert isinstance(style, str), \
895-
'Pass a valid inline HTML style property string to style.'
896-
# noqa outside of type checking.
897-
self.style = style
968+
class GeoJsonPopup(GeoJsonDetail):
969+
"""
970+
Create a popup feature to bind to each element of a GeoJson layer based on
971+
its attributes.
898972
899-
def warn_for_geometry_collections(self):
900-
"""Checks for GeoJson GeometryCollection features to warn user about incompatibility."""
901-
geom_collections = [
902-
feature.get('properties') if feature.get('properties') is not None else key
903-
for key, feature in enumerate(self._parent.data['features'])
904-
if feature['geometry']['type'] == 'GeometryCollection'
905-
]
906-
if any(geom_collections):
907-
warnings.warn(
908-
"GeoJsonTooltip is not configured to render tooltips for GeoJson GeometryCollection geometries. "
909-
"Please consider reworking these features: {} to MultiPolygon for full functionality.\n"
910-
"https://tools.ietf.org/html/rfc7946#page-9".format(geom_collections), UserWarning)
973+
Parameters
974+
----------
975+
fields: list or tuple.
976+
Labels of GeoJson/TopoJson 'properties' or GeoPandas GeoDataFrame
977+
columns you'd like to display.
978+
aliases: list/tuple of strings, same length/order as fields, default None.
979+
Optional aliases you'd like to display in the tooltip as field name
980+
instead of the keys of `fields`.
981+
labels: bool, default True.
982+
Set to False to disable displaying the field names or aliases.
983+
localize: bool, default False.
984+
This will use JavaScript's .toLocaleString() to format 'clean' values
985+
as strings for the user's location; i.e. 1,000,000.00 comma separators,
986+
float truncation, etc.
987+
*Available for most of JavaScript's primitive types (any data you'll
988+
serve into the template).
989+
style: str, default None.
990+
HTML inline style properties like font and colors. Will be applied to
991+
a div with the text in it.
911992
912-
def render(self, **kwargs):
913-
"""Renders the HTML representation of the element."""
914-
if isinstance(self._parent, GeoJson):
915-
keys = tuple(self._parent.data['features'][0]['properties'].keys())
916-
self.warn_for_geometry_collections()
917-
elif isinstance(self._parent, TopoJson):
918-
obj_name = self._parent.object_path.split('.')[-1]
919-
keys = tuple(self._parent.data['objects'][obj_name][
920-
'geometries'][0]['properties'].keys())
921-
else:
922-
raise TypeError('You cannot add a GeoJsonTooltip to anything else '
923-
'than a GeoJson or TopoJson object.')
924-
keys = tuple(x for x in keys if x not in ('style', 'highlight'))
925-
for value in self.fields:
926-
assert value in keys, ('The field {} is not available in the data. '
927-
'Choose from: {}.'.format(value, keys))
928-
super(GeoJsonTooltip, self).render(**kwargs)
993+
Examples
994+
---
995+
gjson = folium.GeoJson(gdf).add_to(m)
996+
997+
folium.features.GeoJsonPopup(fields=['NAME'],
998+
labels=False
999+
).add_to(gjson)
1000+
"""
1001+
1002+
_template = Template(u"""
1003+
{% macro script(this, kwargs) %}
1004+
let name_getter = '{{this._parent.get_name()}}';
1005+
{{ this._parent.get_name() }}.bindPopup(""" + GeoJsonDetail.base_template +
1006+
u""",{{ this.popup_options | tojson | safe }});
1007+
{% endmacro %}
1008+
""")
1009+
1010+
def __init__(self, fields=None, aliases=None, labels=True,
1011+
style="margin: auto;", class_name='foliumpopup', localize=True,
1012+
**kwargs):
1013+
super(GeoJsonPopup, self).__init__(
1014+
fields=fields, aliases=aliases, labels=labels, localize=localize,
1015+
class_name=class_name, style=style)
1016+
self._name = "GeoJsonPopup"
1017+
kwargs.update({'class_name': self.class_name})
1018+
self.popup_options = {
1019+
camelize(key): value for key, value in kwargs.items()}
9291020

9301021

9311022
class Choropleth(FeatureGroup):

0 commit comments

Comments
 (0)