1010import json
1111
1212from branca .colormap import LinearColormap
13- from branca .element import (CssLink , Element , Figure , JavascriptLink , MacroElement ) # noqa
14- from branca .utilities import (_locations_tolist , _parse_size , image_to_url , iter_points , none_max , none_min ) # noqa
13+ from branca .element import (Element , Figure , JavascriptLink , MacroElement )
14+ from branca .utilities import (_locations_tolist , _parse_size , image_to_url ,
15+ none_max , none_min )
16+
17+ from folium .map import (FeatureGroup , Icon , Layer , Marker , Tooltip )
1518
16- from folium .map import FeatureGroup , Icon , Layer , Marker
1719from folium .utilities import get_bounds
1820from folium .vector_layers import PolyLine
1921
@@ -49,10 +51,9 @@ class RegularPolygonMarker(Marker):
4951 radius: int, default 15
5052 Marker radius, in pixels
5153 popup: string or folium.Popup, default None
52- Input text or visualization for object. Can pass either text,
53- or a folium.Popup object.
54- If None, no popup will be displayed.
55-
54+ Input text or visualization for object displayed when clicking.
55+ tooltip: str or folium.Tooltip, default None
56+ Display a text when hovering over the object.
5657
5758 https://humangeo.github.io/leaflet-dvf/
5859
@@ -72,17 +73,16 @@ class RegularPolygonMarker(Marker):
7273 rotation: {{this.rotation}},
7374 radius: {{this.radius}}
7475 }
75- )
76- .addTo({{this._parent.get_name()}});
76+ ).addTo({{this._parent.get_name()}});
7777 {% endmacro %}
7878 """ )
7979
8080 def __init__ (self , location , color = 'black' , opacity = 1 , weight = 2 ,
81- fill_color = 'blue' , fill_opacity = 1 ,
82- number_of_sides = 4 , rotation = 0 , radius = 15 , popup = None ):
81+ fill_color = 'blue' , fill_opacity = 1 , number_of_sides = 4 ,
82+ rotation = 0 , radius = 15 , popup = None , tooltip = None ):
8383 super (RegularPolygonMarker , self ).__init__ (
8484 _locations_tolist (location ),
85- popup = popup
85+ popup = popup , tooltip = tooltip
8686 )
8787 self ._name = 'RegularPolygonMarker'
8888 self .color = color
@@ -325,6 +325,9 @@ class GeoJson(Layer):
325325 How much to simplify the polyline on each zoom level. More means
326326 better performance and smoother look, and less means more accurate
327327 representation. Leaflet defaults to 1.0.
328+ tooltip: GeoJsonTooltip, Tooltip or str, default None
329+ Display a text when hovering over the object. Can utilize the data,
330+ see folium.GeoJsonTooltip for info on how to do that.
328331
329332 Examples
330333 --------
@@ -345,52 +348,46 @@ class GeoJson(Layer):
345348
346349 """
347350 _template = Template (u"""
348- {% macro script(this, kwargs) %}
349-
350- {% if this.highlight %}
351- {{this.get_name()}}_onEachFeature = function onEachFeature(feature, layer) {
352- layer.on({
353- mouseout: function(e) {
354- e.target.setStyle(e.target.feature.properties.style);},
355- mouseover: function(e) {
356- e.target.setStyle(e.target.feature.properties.highlight);},
357- click: function(e) {
358- {{this._parent.get_name()}}.fitBounds(e.target.getBounds());}
359- });
360- };
361- {% endif %}
362-
363- var {{this.get_name()}} = L.geoJson(
364- {% if this.embed %}{{this.style_data()}}{% else %}"{{this.data}}"{% endif %}
365- {% if this.smooth_factor is not none or this.highlight %}
366- , {
367- {% if this.smooth_factor is not none %}
368- smoothFactor:{{this.smooth_factor}}
369- {% endif %}
370-
371- {% if this.highlight %}
372- {% if this.smooth_factor is not none %}
373- ,
374- {% endif %}
375- onEachFeature: {{this.get_name()}}_onEachFeature
376- {% endif %}
377- }
351+ {% macro script(this, kwargs) %}
352+ {% if this.highlight %}
353+ {{this.get_name()}}_onEachFeature = function onEachFeature(feature, layer) {
354+ layer.on({
355+ mouseout: function(e) {
356+ e.target.setStyle(e.target.feature.properties.style);},
357+ mouseover: function(e) {
358+ e.target.setStyle(e.target.feature.properties.highlight);},
359+ click: function(e) {
360+ {{this._parent.get_name()}}.fitBounds(e.target.getBounds());}
361+ });
362+ };
363+ {% endif %}
364+ var {{this.get_name()}} = L.geoJson(
365+ {% if this.embed %}{{this.style_data()}}{% else %}"{{this.data}}"{% endif %}
366+ {% if this.smooth_factor is not none or this.highlight %}
367+ , {
368+ {% if this.smooth_factor is not none %}
369+ smoothFactor:{{this.smooth_factor}}
370+ {% endif %}
371+
372+ {% if this.highlight %}
373+ {% if this.smooth_factor is not none %}
374+ ,
378375 {% endif %}
379- )
380- {% if this.tooltip %}.bindTooltip("{{this.tooltip.__str__()}}"){% endif %}
381- .addTo({{this._parent.get_name()}});
382- {{this.get_name()}}.setStyle(function(feature) {return feature.properties.style;});
383-
384- {% endmacro %}
385- """ ) # noqa
376+ onEachFeature: {{this.get_name()}}_onEachFeature
377+ {% endif %}
378+ }
379+ {% endif %}
380+ ).addTo({{this._parent.get_name()}});
381+ {{this.get_name()}}.setStyle(function(feature) {return feature.properties.style;});
382+ {% endmacro %}
383+ """ ) # noqa
386384
387385 def __init__ (self , data , style_function = None , name = None ,
388386 overlay = True , control = True , show = True ,
389387 smooth_factor = None , highlight_function = None , tooltip = None ):
390388 super (GeoJson , self ).__init__ (name = name , overlay = overlay ,
391389 control = control , show = show )
392390 self ._name = 'GeoJson'
393- self .tooltip = tooltip
394391 if isinstance (data , dict ):
395392 self .embed = True
396393 self .data = data
@@ -410,7 +407,6 @@ def __init__(self, data, style_function=None, name=None,
410407 self .data = json .loads (json .dumps (data .__geo_interface__ )) # noqa
411408 else :
412409 raise ValueError ('Unhandled object {!r}.' .format (data ))
413-
414410 self .style_function = style_function or (lambda x : {})
415411
416412 self .highlight = highlight_function is not None
@@ -419,6 +415,11 @@ def __init__(self, data, style_function=None, name=None,
419415
420416 self .smooth_factor = smooth_factor
421417
418+ if isinstance (tooltip , (GeoJsonTooltip , Tooltip )):
419+ self .add_child (tooltip )
420+ elif tooltip is not None :
421+ self .add_child (Tooltip (tooltip ))
422+
422423 def style_data (self ):
423424 """
424425 Applies `self.style_function` to each feature of `self.data` and
@@ -476,6 +477,9 @@ class TopoJson(Layer):
476477 How much to simplify the polyline on each zoom level. More means
477478 better performance and smoother look, and less means more accurate
478479 representation. Leaflet defaults to 1.0.
480+ tooltip: GeoJsonTooltip, Tooltip or str, default None
481+ Display a text when hovering over the object. Can utilize the data,
482+ see folium.GeoJsonTooltip for info on how to do that.
479483
480484 Examples
481485 --------
@@ -496,29 +500,26 @@ class TopoJson(Layer):
496500
497501 """
498502 _template = Template (u"""
499- {% macro script(this, kwargs) %}
500- var {{this.get_name()}}_data = {{this.style_data()}};
501- var {{this.get_name()}} = L.geoJson(topojson.feature(
502- {{this.get_name()}}_data,
503- {{this.get_name()}}_data.{{this.object_path}})
504- {% if this.smooth_factor is not none %}
505- , {smoothFactor: {{this.smooth_factor}}}
506- {% endif %}
507- )
508- {% if this.tooltip %}.bindTooltip("{{this.tooltip.__str__()}}"){% endif %}
509- .addTo({{this._parent.get_name()}});
510- {{this.get_name()}}.setStyle(function(feature) {return feature.properties.style;});
511-
512- {% endmacro %}
513- """ ) # noqa
503+ {% macro script(this, kwargs) %}
504+ var {{this.get_name()}}_data = {{this.style_data()}};
505+ var {{this.get_name()}} = L.geoJson(topojson.feature(
506+ {{this.get_name()}}_data,
507+ {{this.get_name()}}_data.{{this.object_path}})
508+ {% if this.smooth_factor is not none %}
509+ , {smoothFactor: {{this.smooth_factor}}}
510+ {% endif %}
511+ ).addTo({{this._parent.get_name()}});
512+ {{this.get_name()}}.setStyle(function(feature) {return feature.properties.style;});
513+ {% endmacro %}
514+ """ ) # noqa
514515
515516 def __init__ (self , data , object_path , style_function = None ,
516517 name = None , overlay = True , control = True , show = True ,
517518 smooth_factor = None , tooltip = None ):
518519 super (TopoJson , self ).__init__ (name = name , overlay = overlay ,
519520 control = control , show = show )
520521 self ._name = 'TopoJson'
521- self . tooltip = tooltip
522+
522523 if 'read' in dir (data ):
523524 self .embed = True
524525 self .data = json .load (data )
@@ -538,6 +539,11 @@ def style_function(x):
538539
539540 self .smooth_factor = smooth_factor
540541
542+ if isinstance (tooltip , (GeoJsonTooltip , Tooltip )):
543+ self .add_child (tooltip )
544+ elif tooltip is not None :
545+ self .add_child (Tooltip (tooltip ))
546+
541547 def style_data (self ):
542548 """
543549 Applies self.style_function to each feature of self.data and returns
@@ -595,10 +601,127 @@ def get_bounds(self):
595601 self .data ['transform' ]['translate' ][1 ] + self .data ['transform' ]['scale' ][1 ] * ymax , # noqa
596602 self .data ['transform' ]['translate' ][0 ] + self .data ['transform' ]['scale' ][0 ] * xmax # noqa
597603 ]
598-
599604 ]
600605
601606
607+ class GeoJsonTooltip (Tooltip ):
608+ """
609+ Create a tooltip that uses data from either geojson or topojson.
610+
611+ Parameters
612+ ----------
613+ fields: list or tuple.
614+ Labels of GeoJson/TopoJson 'properties' or GeoPandas GeoDataFrame
615+ columns you'd like to display.
616+ aliases: list/tuple of strings, same length/order as fields, default None.
617+ Optional aliases you'd like to display in the tooltip as field name
618+ instead of the keys of `fields`.
619+ labels: bool, default True.
620+ Set to False to disable displaying the field names or aliases.
621+ localize: bool, default False.
622+ This will use JavaScript's .toLocaleString() to format 'clean' values
623+ as strings for the user's location; i.e. 1,000,000.00 comma separators,
624+ float truncation, etc.
625+ *Available for most of JavaScript's primitive types (any data you'll
626+ serve into the template).
627+ style: str, default None.
628+ HTML inline style properties like font and colors. Will be applied to
629+ a div with the text in it.
630+ sticky: bool, default True
631+ Whether the tooltip should follow the mouse.
632+ **kwargs: Assorted.
633+ These values will map directly to the Leaflet Options. More info
634+ available here: https://leafletjs.com/reference-1.2.0#tooltip
635+
636+ Examples
637+ --------
638+ # Provide fields and aliases, with Style.
639+ >>> Tooltip(
640+ >>> fields=['CNTY_NM', 'census-pop-2015', 'census-md-income-2015'],
641+ >>> aliases=['County', '2015 Census Population', '2015 Median Income'],
642+ >>> localize=True,
643+ >>> style=('background-color: grey; color: white; font-family:'
644+ >>> 'courier new; font-size: 24px; padding: 10px;')
645+ >>> )
646+ # Provide fields, with labels off and fixed tooltip positions.
647+ >>> Tooltip(fields=('CNTY_NM',), labels=False, sticky=False)
648+ """
649+ _template = Template (u"""
650+ {% macro script(this, kwargs) %}
651+ {{ this._parent.get_name() }}.bindTooltip(
652+ function(layer){
653+ // Convert non-primitive to String.
654+ let handleObject = (feature)=>typeof(feature)=='object' ? JSON.stringify(feature) : feature;
655+ let fields = {{ this.fields }};
656+ {% if this.aliases %}
657+ let aliases = {{ this.aliases }};
658+ {% endif %}
659+ return '<table{% if this.style %} style="{{this.style}}"{% endif%}>' +
660+ String(
661+ fields.map(
662+ columnname=>
663+ `<tr style="text-align: left;">{% if this.labels %}
664+ <th style="padding: 4px; padding-right: 10px;">{% if this.aliases %}
665+ ${aliases[fields.indexOf(columnname)]
666+ {% if this.localize %}.toLocaleString(){% endif %}}
667+ {% else %}
668+ ${ columnname{% if this.localize %}.toLocaleString(){% endif %}}
669+ {% endif %}</th>
670+ {% endif %}
671+ <td style="padding: 4px;">${handleObject(layer.feature.properties[columnname])
672+ {% if this.localize %}.toLocaleString(){% endif %}}</td></tr>`
673+ ).join(''))
674+ +'</table>'
675+ }, {{ this.options }});
676+ {% endmacro %}
677+ """ )
678+
679+ def __init__ (self , fields , aliases = None , labels = True ,
680+ localize = False , style = None , sticky = True , ** kwargs ):
681+ super (GeoJsonTooltip , self ).__init__ (
682+ text = '' , style = style , sticky = sticky , ** kwargs
683+ )
684+ self ._name = "GeoJsonTooltip"
685+
686+ assert isinstance (fields , (list , tuple )), "Please pass a list or " \
687+ "tuple to fields."
688+ if aliases is not None :
689+ assert isinstance (aliases , (list , tuple ))
690+ assert len (fields ) == len (aliases ), "fields and aliases must have" \
691+ " the same length."
692+ assert isinstance (labels , bool ), "labels requires a boolean value."
693+ assert isinstance (localize , bool ), "localize must be bool."
694+ assert 'permanent' not in kwargs , "The `permanent` option does not " \
695+ "work with GeoJsonTooltip."
696+
697+ self .fields = fields
698+ self .aliases = aliases
699+ self .labels = labels
700+ self .localize = localize
701+ if style :
702+ assert isinstance (style , str ), \
703+ "Pass a valid inline HTML style property string to style."
704+ # noqa outside of type checking.
705+ self .style = style
706+
707+ def render (self , ** kwargs ):
708+ """Renders the HTML representation of the element."""
709+ if isinstance (self ._parent , GeoJson ):
710+ keys = tuple (self ._parent .data ['features' ][0 ]['properties' ].keys ())
711+ elif isinstance (self ._parent , TopoJson ):
712+ obj_name = self ._parent .object_path .split ('.' )[- 1 ]
713+ keys = tuple (self ._parent .data ['objects' ][obj_name ][
714+ 'geometries' ][0 ]['properties' ].keys ())
715+ else :
716+ raise TypeError ('You cannot add a GeoJsonTooltip to anything else '
717+ 'than a GeoJson or TopoJson object.' )
718+ keys = tuple (x for x in keys if x not in ('style' , 'highlight' ))
719+ for value in self .fields :
720+ assert value in keys , ("The field {} is not available in the data. "
721+ "Choose from: {}." .format (value , keys ))
722+ super (GeoJsonTooltip , self ).render (** kwargs )
723+
724+
602725class DivIcon (MacroElement ):
603726 """
604727 Represents a lightweight icon for markers that uses a simple `div`
0 commit comments