1039 lines
36 KiB
Python
1039 lines
36 KiB
Python
import warnings
|
|
from packaging.version import Version
|
|
from statistics import mean
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
from pandas.api.types import is_datetime64_any_dtype
|
|
|
|
from shapely.geometry import LineString
|
|
|
|
import geopandas
|
|
|
|
_MAP_KWARGS = [
|
|
"location",
|
|
"prefer_canvas",
|
|
"no_touch",
|
|
"disable_3d",
|
|
"png_enabled",
|
|
"zoom_control",
|
|
"crs",
|
|
"zoom_start",
|
|
"left",
|
|
"top",
|
|
"position",
|
|
"min_zoom",
|
|
"max_zoom",
|
|
"min_lat",
|
|
"max_lat",
|
|
"min_lon",
|
|
"max_lon",
|
|
"max_bounds",
|
|
]
|
|
|
|
|
|
def _explore(
|
|
df,
|
|
column=None,
|
|
cmap=None,
|
|
color=None,
|
|
m=None,
|
|
tiles="OpenStreetMap",
|
|
attr=None,
|
|
tooltip=True,
|
|
popup=False,
|
|
highlight=True,
|
|
categorical=False,
|
|
legend=True,
|
|
scheme=None,
|
|
k=5,
|
|
vmin=None,
|
|
vmax=None,
|
|
width="100%",
|
|
height="100%",
|
|
categories=None,
|
|
classification_kwds=None,
|
|
control_scale=True,
|
|
marker_type=None,
|
|
marker_kwds={},
|
|
style_kwds={},
|
|
highlight_kwds={},
|
|
missing_kwds={},
|
|
tooltip_kwds={},
|
|
popup_kwds={},
|
|
legend_kwds={},
|
|
map_kwds={},
|
|
**kwargs,
|
|
):
|
|
"""Interactive map based on GeoPandas and folium/leaflet.js
|
|
|
|
Generate an interactive leaflet map based on :class:`~geopandas.GeoDataFrame`
|
|
|
|
Parameters
|
|
----------
|
|
column : str, np.array, pd.Series (default None)
|
|
The name of the dataframe column, :class:`numpy.array`,
|
|
or :class:`pandas.Series` to be plotted. If :class:`numpy.array` or
|
|
:class:`pandas.Series` are used then it must have same length as dataframe.
|
|
cmap : str, matplotlib.Colormap, branca.colormap or function (default None)
|
|
The name of a colormap recognized by ``matplotlib``, a list-like of colors,
|
|
:class:`matplotlib.colors.Colormap`, a :class:`branca.colormap.ColorMap` or
|
|
function that returns a named color or hex based on the column
|
|
value, e.g.::
|
|
|
|
def my_colormap(value): # scalar value defined in 'column'
|
|
if value > 1:
|
|
return "green"
|
|
return "red"
|
|
|
|
color : str, array-like (default None)
|
|
Named color or a list-like of colors (named or hex).
|
|
m : folium.Map (default None)
|
|
Existing map instance on which to draw the plot.
|
|
tiles : str, xyzservices.TileProvider (default 'OpenStreetMap Mapnik')
|
|
Map tileset to use. Can choose from the list supported by folium, query a
|
|
:class:`xyzservices.TileProvider` by a name from ``xyzservices.providers``,
|
|
pass :class:`xyzservices.TileProvider` object or pass custom XYZ URL.
|
|
The current list of built-in providers (when ``xyzservices`` is not available):
|
|
|
|
``["OpenStreetMap", "CartoDB positron", “CartoDB dark_matter"]``
|
|
|
|
You can pass a custom tileset to Folium by passing a Leaflet-style URL
|
|
to the tiles parameter: ``http://{s}.yourtiles.com/{z}/{x}/{y}.png``.
|
|
Be sure to check their terms and conditions and to provide attribution with
|
|
the ``attr`` keyword.
|
|
attr : str (default None)
|
|
Map tile attribution; only required if passing custom tile URL.
|
|
tooltip : bool, str, int, list (default True)
|
|
Display GeoDataFrame attributes when hovering over the object.
|
|
``True`` includes all columns. ``False`` removes tooltip. Pass string or list of
|
|
strings to specify a column(s). Integer specifies first n columns to be
|
|
included. Defaults to ``True``.
|
|
popup : bool, str, int, list (default False)
|
|
Input GeoDataFrame attributes for object displayed when clicking.
|
|
``True`` includes all columns. ``False`` removes popup. Pass string or list of
|
|
strings to specify a column(s). Integer specifies first n columns to be
|
|
included. Defaults to ``False``.
|
|
highlight : bool (default True)
|
|
Enable highlight functionality when hovering over a geometry.
|
|
categorical : bool (default False)
|
|
If ``False``, ``cmap`` will reflect numerical values of the
|
|
column being plotted. For non-numerical columns, this
|
|
will be set to True.
|
|
legend : bool (default True)
|
|
Plot a legend in choropleth plots.
|
|
Ignored if no ``column`` is given.
|
|
scheme : str (default None)
|
|
Name of a choropleth classification scheme (requires ``mapclassify`` >= 2.4.0).
|
|
A :func:`mapclassify.classify` will be used
|
|
under the hood. Supported are all schemes provided by ``mapclassify`` (e.g.
|
|
``'BoxPlot'``, ``'EqualInterval'``, ``'FisherJenks'``, ``'FisherJenksSampled'``,
|
|
``'HeadTailBreaks'``, ``'JenksCaspall'``, ``'JenksCaspallForced'``,
|
|
``'JenksCaspallSampled'``, ``'MaxP'``, ``'MaximumBreaks'``,
|
|
``'NaturalBreaks'``, ``'Quantiles'``, ``'Percentiles'``, ``'StdMean'``,
|
|
``'UserDefined'``). Arguments can be passed in ``classification_kwds``.
|
|
k : int (default 5)
|
|
Number of classes
|
|
vmin : None or float (default None)
|
|
Minimum value of ``cmap``. If ``None``, the minimum data value
|
|
in the column to be plotted is used.
|
|
vmax : None or float (default None)
|
|
Maximum value of ``cmap``. If ``None``, the maximum data value
|
|
in the column to be plotted is used.
|
|
width : pixel int or percentage string (default: '100%')
|
|
Width of the folium :class:`~folium.folium.Map`. If the argument
|
|
m is given explicitly, width is ignored.
|
|
height : pixel int or percentage string (default: '100%')
|
|
Height of the folium :class:`~folium.folium.Map`. If the argument
|
|
m is given explicitly, height is ignored.
|
|
categories : list-like
|
|
Ordered list-like object of categories to be used for categorical plot.
|
|
classification_kwds : dict (default None)
|
|
Keyword arguments to pass to mapclassify
|
|
control_scale : bool, (default True)
|
|
Whether to add a control scale on the map.
|
|
marker_type : str, folium.Circle, folium.CircleMarker, folium.Marker (default None)
|
|
Allowed string options are ('marker', 'circle', 'circle_marker'). Defaults to
|
|
folium.CircleMarker.
|
|
marker_kwds: dict (default {})
|
|
Additional keywords to be passed to the selected ``marker_type``, e.g.:
|
|
|
|
radius : float (default 2 for ``circle_marker`` and 50 for ``circle``))
|
|
Radius of the circle, in meters (for ``circle``) or pixels
|
|
(for ``circle_marker``).
|
|
fill : bool (default True)
|
|
Whether to fill the ``circle`` or ``circle_marker`` with color.
|
|
icon : folium.map.Icon
|
|
the :class:`folium.map.Icon` object to use to render the marker.
|
|
draggable : bool (default False)
|
|
Set to True to be able to drag the marker around the map.
|
|
|
|
style_kwds : dict (default {})
|
|
Additional style to be passed to folium ``style_function``:
|
|
|
|
stroke : bool (default True)
|
|
Whether to draw stroke along the path. Set it to ``False`` to
|
|
disable borders on polygons or circles.
|
|
color : str
|
|
Stroke color
|
|
weight : int
|
|
Stroke width in pixels
|
|
opacity : float (default 1.0)
|
|
Stroke opacity
|
|
fill : boolean (default True)
|
|
Whether to fill the path with color. Set it to ``False`` to
|
|
disable filling on polygons or circles.
|
|
fillColor : str
|
|
Fill color. Defaults to the value of the color option
|
|
fillOpacity : float (default 0.5)
|
|
Fill opacity.
|
|
style_function : callable
|
|
Function mapping a GeoJson Feature to a style ``dict``.
|
|
|
|
* Style properties :func:`folium.vector_layers.path_options`
|
|
* GeoJson features :class:`GeoDataFrame.__geo_interface__`
|
|
|
|
e.g.::
|
|
|
|
lambda x: {"color":"red" if x["properties"]["gdp_md_est"]<10**6
|
|
else "blue"}
|
|
|
|
Plus all supported by :func:`folium.vector_layers.path_options`. See the
|
|
documentation of :class:`folium.features.GeoJson` for details.
|
|
|
|
highlight_kwds : dict (default {})
|
|
Style to be passed to folium highlight_function. Uses the same keywords
|
|
as ``style_kwds``. When empty, defaults to ``{"fillOpacity": 0.75}``.
|
|
tooltip_kwds : dict (default {})
|
|
Additional keywords to be passed to :class:`folium.features.GeoJsonTooltip`,
|
|
e.g. ``aliases``, ``labels``, or ``sticky``.
|
|
popup_kwds : dict (default {})
|
|
Additional keywords to be passed to :class:`folium.features.GeoJsonPopup`,
|
|
e.g. ``aliases`` or ``labels``.
|
|
legend_kwds : dict (default {})
|
|
Additional keywords to be passed to the legend.
|
|
|
|
Currently supported customisation:
|
|
|
|
caption : string
|
|
Custom caption of the legend. Defaults to the column name.
|
|
|
|
Additional accepted keywords when ``scheme`` is specified:
|
|
|
|
colorbar : bool (default True)
|
|
An option to control the style of the legend. If True, continuous
|
|
colorbar will be used. If False, categorical legend will be used for bins.
|
|
scale : bool (default True)
|
|
Scale bins along the colorbar axis according to the bin edges (True)
|
|
or use the equal length for each bin (False)
|
|
fmt : string (default "{:.2f}")
|
|
A formatting specification for the bin edges of the classes in the
|
|
legend. For example, to have no decimals: ``{"fmt": "{:.0f}"}``. Applies
|
|
if ``colorbar=False``.
|
|
labels : list-like
|
|
A list of legend labels to override the auto-generated labels.
|
|
Needs to have the same number of elements as the number of
|
|
classes (`k`). Applies if ``colorbar=False``.
|
|
interval : boolean (default False)
|
|
An option to control brackets from mapclassify legend.
|
|
If True, open/closed interval brackets are shown in the legend.
|
|
Applies if ``colorbar=False``.
|
|
max_labels : int, default 10
|
|
Maximum number of colorbar tick labels (requires branca>=0.5.0)
|
|
map_kwds : dict (default {})
|
|
Additional keywords to be passed to folium :class:`~folium.folium.Map`,
|
|
e.g. ``dragging``, or ``scrollWheelZoom``.
|
|
|
|
|
|
**kwargs : dict
|
|
Additional options to be passed on to the folium object.
|
|
|
|
Returns
|
|
-------
|
|
m : folium.folium.Map
|
|
folium :class:`~folium.folium.Map` instance
|
|
|
|
Examples
|
|
--------
|
|
>>> import geodatasets
|
|
>>> df = geopandas.read_file(
|
|
... geodatasets.get_path("geoda.chicago_health")
|
|
... )
|
|
>>> df.head(2) # doctest: +SKIP
|
|
ComAreaID ... geometry
|
|
0 35 ... POLYGON ((-87.60914 41.84469, -87.60915 41.844...
|
|
1 36 ... POLYGON ((-87.59215 41.81693, -87.59231 41.816...
|
|
|
|
[2 rows x 87 columns]
|
|
|
|
>>> df.explore("Pop2012", cmap="Blues") # doctest: +SKIP
|
|
"""
|
|
|
|
def _colormap_helper(_cmap, n_resample=None, idx=None):
|
|
"""Helper for MPL deprecation - GH#2596"""
|
|
if not n_resample:
|
|
return cm.get_cmap(_cmap)
|
|
else:
|
|
if MPL_361:
|
|
return cm.get_cmap(_cmap).resampled(n_resample)(idx)
|
|
else:
|
|
return cm.get_cmap(_cmap, n_resample)(idx)
|
|
|
|
try:
|
|
import re
|
|
|
|
import branca as bc
|
|
import folium
|
|
import matplotlib
|
|
import matplotlib.pyplot as plt
|
|
from mapclassify import classify
|
|
from matplotlib import colors
|
|
|
|
# isolate MPL version - GH#2596
|
|
MPL_361 = Version(matplotlib.__version__) >= Version("3.6.1")
|
|
if MPL_361:
|
|
from matplotlib import colormaps as cm
|
|
else:
|
|
from matplotlib import cm
|
|
|
|
except (ImportError, ModuleNotFoundError):
|
|
raise ImportError(
|
|
"The 'folium', 'matplotlib' and 'mapclassify' packages are required for "
|
|
"'explore()'. You can install them using "
|
|
"'conda install -c conda-forge folium matplotlib mapclassify' "
|
|
"or 'pip install folium matplotlib mapclassify'."
|
|
)
|
|
|
|
# xyservices is an optional dependency
|
|
try:
|
|
import xyzservices
|
|
|
|
HAS_XYZSERVICES = True
|
|
except (ImportError, ModuleNotFoundError):
|
|
HAS_XYZSERVICES = False
|
|
|
|
gdf = df.copy()
|
|
|
|
# convert LinearRing to LineString
|
|
rings_mask = df.geom_type == "LinearRing"
|
|
if rings_mask.any():
|
|
gdf.geometry[rings_mask] = gdf.geometry[rings_mask].apply(
|
|
lambda g: LineString(g)
|
|
)
|
|
if isinstance(gdf, geopandas.GeoSeries):
|
|
gdf = gdf.to_frame()
|
|
|
|
if gdf.crs is None:
|
|
kwargs["crs"] = "Simple"
|
|
tiles = None
|
|
elif not gdf.crs.equals(4326):
|
|
gdf = gdf.to_crs(4326)
|
|
|
|
# Fields which are not JSON serializable are coerced to strings
|
|
json_not_supported_cols = gdf.columns[
|
|
[is_datetime64_any_dtype(gdf[c]) for c in gdf.columns]
|
|
].union(gdf.columns[gdf.dtypes == "object"])
|
|
|
|
if len(json_not_supported_cols) > 0:
|
|
gdf = gdf.astype({c: "string" for c in json_not_supported_cols})
|
|
|
|
if not isinstance(gdf.index, pd.MultiIndex) and (
|
|
is_datetime64_any_dtype(gdf.index) or (gdf.index.dtype == "object")
|
|
):
|
|
gdf.index = gdf.index.astype("string")
|
|
|
|
# create folium.Map object
|
|
if m is None:
|
|
# Get bounds to specify location and map extent
|
|
bounds = gdf.total_bounds
|
|
location = kwargs.pop("location", None)
|
|
if location is None and not np.isnan(bounds).all():
|
|
x = mean([bounds[0], bounds[2]])
|
|
y = mean([bounds[1], bounds[3]])
|
|
location = (y, x)
|
|
if "zoom_start" in kwargs.keys():
|
|
fit = False
|
|
else:
|
|
fit = True
|
|
else:
|
|
fit = False
|
|
|
|
# get a subset of kwargs to be passed to folium.Map
|
|
for i in _MAP_KWARGS:
|
|
if i in map_kwds:
|
|
raise ValueError(
|
|
f"'{i}' cannot be specified in 'map_kwds'. "
|
|
f"Use the '{i}={map_kwds[i]}' argument instead."
|
|
)
|
|
map_kwds = {
|
|
**map_kwds,
|
|
**{i: kwargs[i] for i in kwargs.keys() if i in _MAP_KWARGS},
|
|
}
|
|
|
|
if HAS_XYZSERVICES:
|
|
# match provider name string to xyzservices.TileProvider
|
|
if isinstance(tiles, str):
|
|
try:
|
|
tiles = xyzservices.providers.query_name(tiles)
|
|
except ValueError:
|
|
pass
|
|
|
|
if isinstance(tiles, xyzservices.TileProvider):
|
|
attr = attr if attr else tiles.html_attribution
|
|
if "min_zoom" not in map_kwds:
|
|
map_kwds["min_zoom"] = tiles.get("min_zoom", 0)
|
|
if "max_zoom" not in map_kwds:
|
|
map_kwds["max_zoom"] = tiles.get("max_zoom", 18)
|
|
tiles = tiles.build_url(scale_factor="{r}")
|
|
|
|
m = folium.Map(
|
|
location=location,
|
|
control_scale=control_scale,
|
|
tiles=tiles,
|
|
attr=attr,
|
|
width=width,
|
|
height=height,
|
|
**map_kwds,
|
|
)
|
|
|
|
# fit bounds to get a proper zoom level
|
|
if fit:
|
|
m.fit_bounds([[bounds[1], bounds[0]], [bounds[3], bounds[2]]])
|
|
|
|
if gdf.is_empty.all():
|
|
warnings.warn(
|
|
"The GeoSeries you are attempting to plot is "
|
|
"composed of empty geometries. Nothing has been displayed.",
|
|
UserWarning,
|
|
stacklevel=3,
|
|
)
|
|
return m
|
|
|
|
for map_kwd in _MAP_KWARGS:
|
|
kwargs.pop(map_kwd, None)
|
|
|
|
nan_idx = None
|
|
|
|
if column is not None:
|
|
if pd.api.types.is_list_like(column):
|
|
if len(column) != gdf.shape[0]:
|
|
raise ValueError(
|
|
"The GeoDataFrame and given column have different number of rows."
|
|
)
|
|
else:
|
|
column_name = "__plottable_column"
|
|
gdf[column_name] = column
|
|
column = column_name
|
|
elif isinstance(gdf[column].dtype, pd.CategoricalDtype):
|
|
if categories is not None:
|
|
raise ValueError(
|
|
"Cannot specify 'categories' when column has categorical dtype"
|
|
)
|
|
categorical = True
|
|
elif (
|
|
pd.api.types.is_object_dtype(gdf[column])
|
|
or pd.api.types.is_bool_dtype(gdf[column])
|
|
or pd.api.types.is_string_dtype(gdf[column])
|
|
or categories
|
|
):
|
|
categorical = True
|
|
|
|
nan_idx = pd.isna(gdf[column])
|
|
|
|
if categorical:
|
|
cat = pd.Categorical(gdf[column][~nan_idx], categories=categories)
|
|
N = len(cat.categories)
|
|
cmap = cmap if cmap else "tab20"
|
|
|
|
# colormap exists in matplotlib
|
|
if cmap in plt.colormaps():
|
|
color = np.apply_along_axis(
|
|
colors.to_hex,
|
|
1,
|
|
_colormap_helper(cmap, n_resample=N, idx=cat.codes),
|
|
)
|
|
legend_colors = np.apply_along_axis(
|
|
colors.to_hex, 1, _colormap_helper(cmap, n_resample=N, idx=range(N))
|
|
)
|
|
|
|
# colormap is matplotlib.Colormap
|
|
elif isinstance(cmap, colors.Colormap):
|
|
color = np.apply_along_axis(colors.to_hex, 1, cmap(cat.codes))
|
|
legend_colors = np.apply_along_axis(colors.to_hex, 1, cmap(range(N)))
|
|
|
|
# custom list of colors
|
|
elif pd.api.types.is_list_like(cmap):
|
|
if N > len(cmap):
|
|
cmap = cmap * (N // len(cmap) + 1)
|
|
color = np.take(cmap, cat.codes)
|
|
legend_colors = np.take(cmap, range(N))
|
|
|
|
else:
|
|
raise ValueError(
|
|
"'cmap' is invalid. For categorical plots, pass either valid "
|
|
"named matplotlib colormap or a list-like of colors."
|
|
)
|
|
|
|
elif callable(cmap):
|
|
# List of colors based on Branca colormaps or self-defined functions
|
|
color = [cmap(x) for x in df[column]]
|
|
|
|
else:
|
|
vmin = gdf[column].min() if vmin is None else vmin
|
|
vmax = gdf[column].max() if vmax is None else vmax
|
|
|
|
# get bins
|
|
if scheme is not None:
|
|
if classification_kwds is None:
|
|
classification_kwds = {}
|
|
if "k" not in classification_kwds:
|
|
classification_kwds["k"] = k
|
|
|
|
binning = classify(
|
|
np.asarray(gdf[column][~nan_idx]), scheme, **classification_kwds
|
|
)
|
|
color = np.apply_along_axis(
|
|
colors.to_hex,
|
|
1,
|
|
_colormap_helper(cmap, n_resample=binning.k, idx=binning.yb),
|
|
)
|
|
|
|
else:
|
|
bins = np.linspace(vmin, vmax, 257)[1:]
|
|
binning = classify(
|
|
np.asarray(gdf[column][~nan_idx]), "UserDefined", bins=bins
|
|
)
|
|
|
|
color = np.apply_along_axis(
|
|
colors.to_hex,
|
|
1,
|
|
_colormap_helper(cmap, n_resample=256, idx=binning.yb),
|
|
)
|
|
|
|
# set default style
|
|
if "fillOpacity" not in style_kwds:
|
|
style_kwds["fillOpacity"] = 0.5
|
|
if "weight" not in style_kwds:
|
|
style_kwds["weight"] = 2
|
|
if "style_function" in style_kwds:
|
|
style_kwds_function = style_kwds["style_function"]
|
|
if not callable(style_kwds_function):
|
|
raise ValueError("'style_function' has to be a callable")
|
|
style_kwds.pop("style_function")
|
|
else:
|
|
|
|
def _no_style(x):
|
|
return {}
|
|
|
|
style_kwds_function = _no_style
|
|
|
|
# specify color
|
|
if color is not None:
|
|
if (
|
|
isinstance(color, str)
|
|
and isinstance(gdf, geopandas.GeoDataFrame)
|
|
and color in gdf.columns
|
|
): # use existing column
|
|
|
|
def _style_color(x):
|
|
base_style = {
|
|
"fillColor": x["properties"][color],
|
|
**style_kwds,
|
|
}
|
|
return {
|
|
**base_style,
|
|
**style_kwds_function(x),
|
|
}
|
|
|
|
style_function = _style_color
|
|
else: # assign new column
|
|
if isinstance(gdf, geopandas.GeoSeries):
|
|
gdf = geopandas.GeoDataFrame(geometry=gdf)
|
|
|
|
if nan_idx is not None and nan_idx.any():
|
|
nan_color = missing_kwds.pop("color", None)
|
|
|
|
gdf["__folium_color"] = nan_color
|
|
gdf.loc[~nan_idx, "__folium_color"] = color
|
|
else:
|
|
gdf["__folium_color"] = color
|
|
|
|
stroke_color = style_kwds.pop("color", None)
|
|
if not stroke_color:
|
|
|
|
def _style_column(x):
|
|
base_style = {
|
|
"fillColor": x["properties"]["__folium_color"],
|
|
"color": x["properties"]["__folium_color"],
|
|
**style_kwds,
|
|
}
|
|
return {
|
|
**base_style,
|
|
**style_kwds_function(x),
|
|
}
|
|
|
|
style_function = _style_column
|
|
else:
|
|
|
|
def _style_stroke(x):
|
|
base_style = {
|
|
"fillColor": x["properties"]["__folium_color"],
|
|
"color": stroke_color,
|
|
**style_kwds,
|
|
}
|
|
return {
|
|
**base_style,
|
|
**style_kwds_function(x),
|
|
}
|
|
|
|
style_function = _style_stroke
|
|
else: # use folium default
|
|
|
|
def _style_default(x):
|
|
return {**style_kwds, **style_kwds_function(x)}
|
|
|
|
style_function = _style_default
|
|
|
|
if highlight:
|
|
if "fillOpacity" not in highlight_kwds:
|
|
highlight_kwds["fillOpacity"] = 0.75
|
|
|
|
def _style_highlight(x):
|
|
return {**highlight_kwds}
|
|
|
|
highlight_function = _style_highlight
|
|
else:
|
|
highlight_function = None
|
|
|
|
# define default for points
|
|
if marker_type is None:
|
|
marker_type = "circle_marker"
|
|
|
|
marker = marker_type
|
|
if isinstance(marker_type, str):
|
|
if marker_type == "marker":
|
|
marker = folium.Marker(**marker_kwds)
|
|
elif marker_type == "circle":
|
|
marker = folium.Circle(**marker_kwds)
|
|
elif marker_type == "circle_marker":
|
|
marker_kwds["radius"] = marker_kwds.get("radius", 2)
|
|
marker_kwds["fill"] = marker_kwds.get("fill", True)
|
|
marker = folium.CircleMarker(**marker_kwds)
|
|
else:
|
|
raise ValueError(
|
|
"Only 'marker', 'circle', and 'circle_marker' are "
|
|
"supported as marker values"
|
|
)
|
|
|
|
# remove additional geometries
|
|
if isinstance(gdf, geopandas.GeoDataFrame):
|
|
non_active_geoms = [
|
|
name
|
|
for name, val in (gdf.dtypes == "geometry").items()
|
|
if val and name != gdf.geometry.name
|
|
]
|
|
gdf = gdf.drop(columns=non_active_geoms)
|
|
|
|
# prepare tooltip and popup
|
|
if isinstance(gdf, geopandas.GeoDataFrame):
|
|
# add named index to the tooltip
|
|
if gdf.index.name is not None:
|
|
gdf = gdf.reset_index()
|
|
# specify fields to show in the tooltip
|
|
tooltip = _tooltip_popup("tooltip", tooltip, gdf, **tooltip_kwds)
|
|
popup = _tooltip_popup("popup", popup, gdf, **popup_kwds)
|
|
else:
|
|
tooltip = None
|
|
popup = None
|
|
# escape the curly braces {{}} for jinja2 templates
|
|
feature_collection = gdf[
|
|
~(gdf.geometry.isna() | gdf.geometry.is_empty) # drop missing or empty geoms
|
|
].__geo_interface__
|
|
for feature in feature_collection["features"]:
|
|
for k in feature["properties"]:
|
|
# escape the curly braces in values
|
|
if isinstance(feature["properties"][k], str):
|
|
feature["properties"][k] = re.sub(
|
|
r"\{{2,}",
|
|
lambda x: "{% raw %}" + x.group(0) + "{% endraw %}",
|
|
feature["properties"][k],
|
|
)
|
|
|
|
# add dataframe to map
|
|
folium.GeoJson(
|
|
feature_collection,
|
|
tooltip=tooltip,
|
|
popup=popup,
|
|
marker=marker,
|
|
style_function=style_function,
|
|
highlight_function=highlight_function,
|
|
**kwargs,
|
|
).add_to(m)
|
|
|
|
if legend:
|
|
# NOTE: overlaps will be resolved in branca #88
|
|
caption = column if not column == "__plottable_column" else ""
|
|
caption = legend_kwds.pop("caption", caption)
|
|
if categorical:
|
|
categories = cat.categories.to_list()
|
|
legend_colors = legend_colors.tolist()
|
|
|
|
if nan_idx.any() and nan_color:
|
|
categories.append(missing_kwds.pop("label", "NaN"))
|
|
legend_colors.append(nan_color)
|
|
|
|
_categorical_legend(m, caption, categories, legend_colors)
|
|
elif column is not None:
|
|
cbar = legend_kwds.pop("colorbar", True)
|
|
colormap_kwds = {}
|
|
if "max_labels" in legend_kwds:
|
|
colormap_kwds["max_labels"] = legend_kwds.pop("max_labels")
|
|
if scheme:
|
|
cb_colors = np.apply_along_axis(
|
|
colors.to_hex,
|
|
1,
|
|
_colormap_helper(cmap, n_resample=binning.k, idx=range(binning.k)),
|
|
)
|
|
if cbar:
|
|
if legend_kwds.pop("scale", True):
|
|
index = [vmin] + binning.bins.tolist()
|
|
else:
|
|
index = None
|
|
colorbar = bc.colormap.StepColormap(
|
|
cb_colors,
|
|
vmin=vmin,
|
|
vmax=vmax,
|
|
caption=caption,
|
|
index=index,
|
|
**colormap_kwds,
|
|
)
|
|
else:
|
|
fmt = legend_kwds.pop("fmt", "{:.2f}")
|
|
if "labels" in legend_kwds:
|
|
categories = legend_kwds["labels"]
|
|
else:
|
|
categories = binning.get_legend_classes(fmt)
|
|
show_interval = legend_kwds.pop("interval", False)
|
|
if not show_interval:
|
|
categories = [c[1:-1] for c in categories]
|
|
|
|
if nan_idx.any() and nan_color:
|
|
categories.append(missing_kwds.pop("label", "NaN"))
|
|
cb_colors = np.append(cb_colors, nan_color)
|
|
_categorical_legend(m, caption, categories, cb_colors)
|
|
|
|
else:
|
|
if isinstance(cmap, bc.colormap.ColorMap):
|
|
colorbar = cmap
|
|
else:
|
|
mp_cmap = _colormap_helper(cmap)
|
|
cb_colors = np.apply_along_axis(
|
|
colors.to_hex, 1, mp_cmap(range(mp_cmap.N))
|
|
)
|
|
|
|
# linear legend
|
|
if mp_cmap.N > 20:
|
|
colorbar = bc.colormap.LinearColormap(
|
|
cb_colors,
|
|
vmin=vmin,
|
|
vmax=vmax,
|
|
caption=caption,
|
|
**colormap_kwds,
|
|
)
|
|
|
|
# steps
|
|
else:
|
|
colorbar = bc.colormap.StepColormap(
|
|
cb_colors,
|
|
vmin=vmin,
|
|
vmax=vmax,
|
|
caption=caption,
|
|
**colormap_kwds,
|
|
)
|
|
|
|
if cbar:
|
|
if nan_idx.any() and nan_color:
|
|
_categorical_legend(
|
|
m, "", [missing_kwds.pop("label", "NaN")], [nan_color]
|
|
)
|
|
m.add_child(colorbar)
|
|
|
|
return m
|
|
|
|
|
|
def _tooltip_popup(type, fields, gdf, **kwds):
|
|
"""get tooltip or popup"""
|
|
import folium
|
|
|
|
# specify fields to show in the tooltip
|
|
if fields is False or fields is None or fields == 0:
|
|
return None
|
|
else:
|
|
if fields is True:
|
|
fields = gdf.columns.drop(gdf.geometry.name).to_list()
|
|
elif isinstance(fields, int):
|
|
fields = gdf.columns.drop(gdf.geometry.name).to_list()[:fields]
|
|
elif isinstance(fields, str):
|
|
fields = [fields]
|
|
|
|
for field in ["__plottable_column", "__folium_color"]:
|
|
if field in fields:
|
|
fields.remove(field)
|
|
|
|
# Cast fields to str
|
|
fields = list(map(str, fields))
|
|
if type == "tooltip":
|
|
return folium.GeoJsonTooltip(fields, **kwds)
|
|
elif type == "popup":
|
|
return folium.GeoJsonPopup(fields, **kwds)
|
|
|
|
|
|
def _categorical_legend(m, title, categories, colors):
|
|
"""
|
|
Add categorical legend to a map
|
|
|
|
The implementation is using the code originally written by Michel Metran
|
|
(@michelmetran) and released on GitHub
|
|
(https://github.com/michelmetran/package_folium) under MIT license.
|
|
|
|
Copyright (c) 2020 Michel Metran
|
|
|
|
Parameters
|
|
----------
|
|
m : folium.Map
|
|
Existing map instance on which to draw the plot
|
|
title : str
|
|
title of the legend (e.g. column name)
|
|
categories : list-like
|
|
list of categories
|
|
colors : list-like
|
|
list of colors (in the same order as categories)
|
|
"""
|
|
|
|
# Header to Add
|
|
head = """
|
|
{% macro header(this, kwargs) %}
|
|
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
|
|
<script>$( function() {
|
|
$( ".maplegend" ).draggable({
|
|
start: function (event, ui) {
|
|
$(this).css({
|
|
right: "auto",
|
|
top: "auto",
|
|
bottom: "auto"
|
|
});
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
<style type='text/css'>
|
|
.maplegend {
|
|
position: absolute;
|
|
z-index:9999;
|
|
background-color: rgba(255, 255, 255, .8);
|
|
border-radius: 5px;
|
|
box-shadow: 0 0 15px rgba(0,0,0,0.2);
|
|
padding: 10px;
|
|
font: 12px/14px Arial, Helvetica, sans-serif;
|
|
right: 10px;
|
|
bottom: 20px;
|
|
}
|
|
.maplegend .legend-title {
|
|
text-align: left;
|
|
margin-bottom: 5px;
|
|
font-weight: bold;
|
|
}
|
|
.maplegend .legend-scale ul {
|
|
margin: 0;
|
|
margin-bottom: 0px;
|
|
padding: 0;
|
|
float: left;
|
|
list-style: none;
|
|
}
|
|
.maplegend .legend-scale ul li {
|
|
list-style: none;
|
|
margin-left: 0;
|
|
line-height: 16px;
|
|
margin-bottom: 2px;
|
|
}
|
|
.maplegend ul.legend-labels li span {
|
|
display: block;
|
|
float: left;
|
|
height: 14px;
|
|
width: 14px;
|
|
margin-right: 5px;
|
|
margin-left: 0;
|
|
border: 0px solid #ccc;
|
|
}
|
|
.maplegend .legend-source {
|
|
color: #777;
|
|
clear: both;
|
|
}
|
|
.maplegend a {
|
|
color: #777;
|
|
}
|
|
</style>
|
|
{% endmacro %}
|
|
"""
|
|
import branca as bc
|
|
|
|
# Add CSS (on Header)
|
|
macro = bc.element.MacroElement()
|
|
macro._template = bc.element.Template(head)
|
|
m.get_root().add_child(macro)
|
|
|
|
body = f"""
|
|
<div id='maplegend {title}' class='maplegend'>
|
|
<div class='legend-title'>{title}</div>
|
|
<div class='legend-scale'>
|
|
<ul class='legend-labels'>"""
|
|
|
|
# Loop Categories
|
|
for label, color in zip(categories, colors):
|
|
body += f"""
|
|
<li><span style='background:{color}'></span>{label}</li>"""
|
|
|
|
body += """
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
"""
|
|
|
|
# Add Body
|
|
body = bc.element.Element(body, "legend")
|
|
m.get_root().html.add_child(body)
|
|
|
|
|
|
def _explore_geoseries(
|
|
s,
|
|
color=None,
|
|
m=None,
|
|
tiles="OpenStreetMap",
|
|
attr=None,
|
|
highlight=True,
|
|
width="100%",
|
|
height="100%",
|
|
control_scale=True,
|
|
marker_type=None,
|
|
marker_kwds={},
|
|
style_kwds={},
|
|
highlight_kwds={},
|
|
map_kwds={},
|
|
**kwargs,
|
|
):
|
|
"""Interactive map based on GeoPandas and folium/leaflet.js
|
|
|
|
Generate an interactive leaflet map based on :class:`~geopandas.GeoSeries`
|
|
|
|
Parameters
|
|
----------
|
|
color : str, array-like (default None)
|
|
Named color or a list-like of colors (named or hex).
|
|
m : folium.Map (default None)
|
|
Existing map instance on which to draw the plot.
|
|
tiles : str, xyzservices.TileProvider (default 'OpenStreetMap Mapnik')
|
|
Map tileset to use. Can choose from the list supported by folium, query a
|
|
:class:`xyzservices.TileProvider` by a name from ``xyzservices.providers``,
|
|
pass :class:`xyzservices.TileProvider` object or pass custom XYZ URL.
|
|
The current list of built-in providers (when ``xyzservices`` is not available):
|
|
|
|
``["OpenStreetMap", "CartoDB positron", “CartoDB dark_matter"]``
|
|
|
|
You can pass a custom tileset to Folium by passing a Leaflet-style URL
|
|
to the tiles parameter: ``http://{s}.yourtiles.com/{z}/{x}/{y}.png``.
|
|
Be sure to check their terms and conditions and to provide attribution with
|
|
the ``attr`` keyword.
|
|
attr : str (default None)
|
|
Map tile attribution; only required if passing custom tile URL.
|
|
highlight : bool (default True)
|
|
Enable highlight functionality when hovering over a geometry.
|
|
width : pixel int or percentage string (default: '100%')
|
|
Width of the folium :class:`~folium.folium.Map`. If the argument
|
|
m is given explicitly, width is ignored.
|
|
height : pixel int or percentage string (default: '100%')
|
|
Height of the folium :class:`~folium.folium.Map`. If the argument
|
|
m is given explicitly, height is ignored.
|
|
control_scale : bool, (default True)
|
|
Whether to add a control scale on the map.
|
|
marker_type : str, folium.Circle, folium.CircleMarker, folium.Marker (default None)
|
|
Allowed string options are ('marker', 'circle', 'circle_marker'). Defaults to
|
|
folium.Marker.
|
|
marker_kwds: dict (default {})
|
|
Additional keywords to be passed to the selected ``marker_type``, e.g.:
|
|
|
|
radius : float
|
|
Radius of the circle, in meters (for ``'circle'``) or pixels
|
|
(for ``circle_marker``).
|
|
icon : folium.map.Icon
|
|
the :class:`folium.map.Icon` object to use to render the marker.
|
|
draggable : bool (default False)
|
|
Set to True to be able to drag the marker around the map.
|
|
|
|
style_kwds : dict (default {})
|
|
Additional style to be passed to folium ``style_function``:
|
|
|
|
stroke : bool (default True)
|
|
Whether to draw stroke along the path. Set it to ``False`` to
|
|
disable borders on polygons or circles.
|
|
color : str
|
|
Stroke color
|
|
weight : int
|
|
Stroke width in pixels
|
|
opacity : float (default 1.0)
|
|
Stroke opacity
|
|
fill : boolean (default True)
|
|
Whether to fill the path with color. Set it to ``False`` to
|
|
disable filling on polygons or circles.
|
|
fillColor : str
|
|
Fill color. Defaults to the value of the color option
|
|
fillOpacity : float (default 0.5)
|
|
Fill opacity.
|
|
style_function : callable
|
|
Function mapping a GeoJson Feature to a style ``dict``.
|
|
|
|
* Style properties :func:`folium.vector_layers.path_options`
|
|
* GeoJson features :class:`GeoSeries.__geo_interface__`
|
|
|
|
e.g.::
|
|
|
|
lambda x: {"color":"red" if x["properties"]["gdp_md_est"]<10**6
|
|
else "blue"}
|
|
|
|
|
|
Plus all supported by :func:`folium.vector_layers.path_options`. See the
|
|
documentation of :class:`folium.features.GeoJson` for details.
|
|
|
|
highlight_kwds : dict (default {})
|
|
Style to be passed to folium highlight_function. Uses the same keywords
|
|
as ``style_kwds``. When empty, defaults to ``{"fillOpacity": 0.75}``.
|
|
map_kwds : dict (default {})
|
|
Additional keywords to be passed to folium :class:`~folium.folium.Map`,
|
|
e.g. ``dragging``, or ``scrollWheelZoom``.
|
|
|
|
**kwargs : dict
|
|
Additional options to be passed on to the folium.
|
|
|
|
Returns
|
|
-------
|
|
m : folium.folium.Map
|
|
folium :class:`~folium.folium.Map` instance
|
|
|
|
"""
|
|
return _explore(
|
|
s,
|
|
color=color,
|
|
m=m,
|
|
tiles=tiles,
|
|
attr=attr,
|
|
highlight=highlight,
|
|
width=width,
|
|
height=height,
|
|
control_scale=control_scale,
|
|
marker_type=marker_type,
|
|
marker_kwds=marker_kwds,
|
|
style_kwds=style_kwds,
|
|
highlight_kwds=highlight_kwds,
|
|
map_kwds=map_kwds,
|
|
**kwargs,
|
|
)
|