diff --git a/examples/explore.ipynb b/examples/explore.ipynb new file mode 100644 index 00000000..0175a48d --- /dev/null +++ b/examples/explore.ipynb @@ -0,0 +1,673 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "a76948e2-eda2-4c6f-8fe2-341b67ac5531", + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2\n", + "\n", + "\n", + "import geodatasets\n", + "import geopandas as gpd\n", + "import numpy as np\n", + "from mapclassify import classify\n", + "\n", + "import lonboard.geopandas # noqa: F401\n", + "from lonboard import PolygonLayer" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1e09eb28-7b9a-4dd6-a11a-f9d26bd5e793", + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext watermark\n", + "%watermark -a 'eli knaap' -iv" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3902a38a-84c2-492a-b8b8-fad2b4afef72", + "metadata": {}, + "outputs": [], + "source": [ + "gdf = gpd.read_file(geodatasets.get_path(\"geoda.milwaukee1\"))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9b0db679-e16b-4111-b24f-19de0c544fc5", + "metadata": {}, + "outputs": [], + "source": [ + "gdf = gdf.to_crs(gdf.estimate_utm_crs())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0e071041-a9a6-4d9a-9c3f-e8b252bda744", + "metadata": {}, + "outputs": [], + "source": [ + "gdf = gdf[[\"HH_INC\", \"geometry\"]]" + ] + }, + { + "cell_type": "markdown", + "id": "d3b67d78-8ddc-4691-9f09-3fbb81a65cd0", + "metadata": {}, + "source": [ + "## Simple Map" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11c477e4-b3c0-489c-a85c-2a9ac9e48d8c", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "gdf.explore()" + ] + }, + { + "cell_type": "markdown", + "id": "93da152a-6409-4764-ab24-f773e3d09fb9", + "metadata": {}, + "source": [ + "## boorish (classless :P) choropleth" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "487cafd3-4735-4c2c-b19f-6ff6dcdfe93c", + "metadata": {}, + "outputs": [], + "source": [ + "gdf.explore(\"HH_INC\", tiles=\"CartoDB Darkmatter\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f044e655-32be-4ce1-b015-223ed9c71de1", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "gdf.lb.explore(\n", + " column=\"HH_INC\",\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "03ea7ff8-6373-426c-a046-0398bab5a114", + "metadata": {}, + "outputs": [], + "source": [ + "gdf.assign(catinc=gdf.HH_INC.astype(\"str\")).lb.explore(\n", + " column=\"catinc\",\n", + " categorical=True,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "37f82819-37f7-4e32-abc2-e26b8b38c24a", + "metadata": {}, + "source": [ + "## simple classified choropleth" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d39aeef9-3d1f-433c-b053-fc4f12fe81c7", + "metadata": {}, + "outputs": [], + "source": [ + "gdf.explore(\n", + " column=\"HH_INC\",\n", + " scheme=\"quantiles\",\n", + " k=6,\n", + " cmap=\"YlOrBr\",\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e70b95c3-09b2-40be-8f5e-fd1482388be2", + "metadata": {}, + "outputs": [], + "source": [ + "gdf.lb.explore(\n", + " column=\"HH_INC\",\n", + " cmap=\"YlOrBr\",\n", + " scheme=\"quantiles\",\n", + " alpha=0.5,\n", + " layer_kwargs={\"opacity\": 1},\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "148bce49-d04d-4c74-b246-7d5125622ede", + "metadata": {}, + "outputs": [], + "source": [ + "gdf.lb.explore(\n", + " column=\"HH_INC\",\n", + " cmap=\"YlOrBr\",\n", + " scheme=\"quantiles\",\n", + " alpha=1,\n", + " layer_kwargs={\"opacity\": 0.5},\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "d43d7314-5db4-4cdc-89fd-3733698c5c10", + "metadata": {}, + "source": [ + "### custom classification" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7eb2b472-a17f-4ff3-8853-3c3a2e7dc058", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "classify_kwds = {\"bins\": [20000, 40000, 800000, 2000000]}\n", + "\n", + "gdf.explore(\n", + " column=\"HH_INC\",\n", + " cmap=\"YlOrBr\",\n", + " scheme=\"user_defined\",\n", + " classification_kwds=classify_kwds,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ad5c4e9f-7dcc-4144-b5ba-77987cea0def", + "metadata": {}, + "outputs": [], + "source": [ + "gdf.lb.explore(\n", + " column=\"HH_INC\",\n", + " cmap=\"YlOrBr\",\n", + " scheme=\"user_defined\",\n", + " classification_kwds=classify_kwds,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "998168a1-fa09-4b01-a4e3-77acf5de22fb", + "metadata": {}, + "source": [ + "## compose with lines and points" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7bfeed99-4ce3-4353-93e5-43c6b72c552e", + "metadata": {}, + "outputs": [], + "source": [ + "linedf = gpd.GeoDataFrame(geometry=gdf.boundary)\n", + "linedf[\"len\"] = linedf.length" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13d4ca32-c4c7-44e7-b108-4a82e54da0ac", + "metadata": {}, + "outputs": [], + "source": [ + "m = linedf.explore(\n", + " column=\"len\",\n", + " cmap=\"viridis_r\",\n", + " scheme=\"quantiles\",\n", + " k=10,\n", + " tiles=\"CartoDB Darkmatter\",\n", + ")\n", + "gdf.set_geometry(gdf.centroid).explore(color=\"magenta\", m=m)\n", + "m" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "54df1700-2971-4813-9842-aa0116b46e20", + "metadata": {}, + "outputs": [], + "source": [ + "m = linedf.lb.explore(column=\"len\", cmap=\"viridis_r\", scheme=\"quantiles\", k=10)\n", + "gdf.set_geometry(gdf.centroid).lb.explore(color=\"magenta\", m=m)\n", + "\n", + "# cant do this part unless you use fit_bounds?\n", + "m.set_view_state(zoom=10.5, longitude=-87.85, latitude=43.05)\n", + "m" + ] + }, + { + "cell_type": "markdown", + "id": "732114af-7e34-49ee-94e1-5eb04cd505b1", + "metadata": {}, + "source": [ + "## categorical" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1018dc7c-7dca-4e24-80bc-e908216e416f", + "metadata": {}, + "outputs": [], + "source": [ + "# random categories\n", + "gdf[\"i\"] = gdf.index.values" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b907bc91-f3a2-4513-a9ce-1718acf29489", + "metadata": {}, + "outputs": [], + "source": [ + "gdf.explore(\n", + " \"i\",\n", + " categorical=True,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a7f86337-b4ed-4ac6-929c-13d053f21767", + "metadata": {}, + "outputs": [], + "source": [ + "gdf.assign(i=gdf[\"i\"].astype(str)).lb.explore(\n", + " column=\"i\",\n", + " # categorical=True,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d6a5442e-e60a-4587-9ab5-c91c3fa29a44", + "metadata": {}, + "outputs": [], + "source": [ + "gdf.assign(icat=gdf[\"i\"].astype(\"category\")).lb.explore(\n", + " column=\"icat\",\n", + " categorical=True,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "73ccf36d-a9af-4bf6-853e-fb117e188e4e", + "metadata": {}, + "outputs": [], + "source": [ + "categories = classify(gdf.HH_INC, scheme=\"quantiles\", k=5).yb" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e71e3277-2f5d-4129-92b6-d0431f39dc92", + "metadata": {}, + "outputs": [], + "source": [ + "# categories that are actually quantiles\n", + "gdf[\"q5\"] = categories\n", + "gdf[\"q5\"] = gdf[\"q5\"].astype(\"category\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "90cb3055-4990-41b1-a0f0-de3ebcd8caa3", + "metadata": {}, + "outputs": [], + "source": [ + "gdf.explore(\"q5\", categorical=True, cmap=\"tab20b\", tiles=\"CartoDB Positron\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9bd34a9c-6a45-4f53-a9ca-19324e6aa32d", + "metadata": {}, + "outputs": [], + "source": [ + "m = gdf.lb.explore(\n", + " column=\"q5\",\n", + " categorical=True,\n", + " cmap=\"tab20b\",\n", + " nan_color=[0, 0, 0, 0],\n", + " tiles=\"CartoDB Positron\",\n", + ")\n", + "m" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c54f7abe-ca10-467a-baf0-ff6c499ea4e8", + "metadata": {}, + "outputs": [], + "source": [ + "m = gdf.lb.explore(column=\"q5\", categorical=True, cmap=\"RdBu\", alpha=0.5)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "433064c8-6f54-43fe-9e73-1754100a6d50", + "metadata": {}, + "outputs": [], + "source": [ + "m" + ] + }, + { + "cell_type": "markdown", + "id": "4c6c26e7-3cc1-434f-ae00-84a1544bbf0d", + "metadata": {}, + "source": [ + "## animated choropleth" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "074b0cef-484d-4f63-b258-e71d77aabc36", + "metadata": {}, + "outputs": [], + "source": [ + "m = gdf.lb.explore(\n", + " categorical=True,\n", + " color=\"orange\",\n", + " map_kwargs={\"show_side_panel\": True},\n", + ")\n", + "m" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fc26c67d-35f2-43c2-9163-f8d4676ef559", + "metadata": {}, + "outputs": [], + "source": [ + "m.layers[0].get_fill_color = \"blue\"" + ] + }, + { + "cell_type": "markdown", + "id": "09460532-e6b1-4db1-b7f0-0969cfc18603", + "metadata": {}, + "source": [ + "without rerendering the map you can update the color" + ] + }, + { + "cell_type": "markdown", + "id": "7b45e650-b4b6-4e07-a753-a3dd5102b09c", + "metadata": {}, + "source": [ + "(this updates *both* maps, including the cell above)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3679be91-b2da-439c-8e31-f9fecbca1989", + "metadata": {}, + "outputs": [], + "source": [ + "m" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "77350cf2-3d15-47ec-a517-aa532355b77b", + "metadata": {}, + "outputs": [], + "source": [ + "from time import sleep\n", + "\n", + "for _ in range(5):\n", + " for color in [\"yellow\", \"red\", \"blue\"]:\n", + " m.layers[0].get_fill_color = color\n", + " sleep(0.3)\n", + " sleep(1)" + ] + }, + { + "cell_type": "markdown", + "id": "d456a881-5748-4ea0-9b88-1e3890fc1bf8", + "metadata": {}, + "source": [ + "change color in a timed loop like a gif" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "48c5bb2c-f908-47d8-8e3d-62d7eb7c9cd1", + "metadata": {}, + "outputs": [], + "source": [ + "from ipywidgets import widget\n", + "from mapclassify import classify\n", + "from mapclassify.util import get_color_array\n", + "from matplotlib import colormaps" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9bef0a45-e050-48c6-a295-2cfe55f7787f", + "metadata": {}, + "outputs": [], + "source": [ + "def choro(\n", + " vals: np.ndarray,\n", + " classifier: str,\n", + " cmap: str,\n", + " k: int,\n", + " layer: PolygonLayer,\n", + ") -> widget:\n", + " layer.get_fill_color = get_color_array(vals, classifier, k=k, cmap=cmap)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0577a3a1-74e2-4d89-b1ce-e4ddd27340c4", + "metadata": {}, + "outputs": [], + "source": [ + "m = gdf.lb.explore()\n", + "m" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1ba6de4d-c2e3-45ae-a444-d2c78606064b", + "metadata": {}, + "outputs": [], + "source": [ + "from mapclassify._classify_API import _classifiers" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a766ea7e-f400-4bed-8fcd-6cf332174f6e", + "metadata": {}, + "outputs": [], + "source": [ + "from ipywidgets import fixed, interact\n", + "\n", + "interact(\n", + " choro,\n", + " vals=fixed(gdf.HH_INC),\n", + " classifier=list(_classifiers.keys()),\n", + " k=range(3, 10),\n", + " cmap=list(colormaps.keys()),\n", + " layer=fixed(m.layers[0]),\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "73c7a823-c3fd-4ade-aa73-5709b0ee29d7", + "metadata": {}, + "outputs": [], + "source": [ + "gdf.loc[0:200, \"HH_INC\"] = np.nan\n", + "\n", + "m = gdf.lb.explore(\n", + " column=\"HH_INC\",\n", + " cmap=\"Accent\",\n", + " nan_color=[0, 0, 0, 0],\n", + ")\n", + "m" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "89bca34c-7123-44ca-be2c-e451f946ec38", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "id": "5b1833da-1fbe-4f35-96bf-d1d63d350e25", + "metadata": {}, + "source": [ + "## Additional features not yet implemented" + ] + }, + { + "cell_type": "markdown", + "id": "e1f9f5c8-9a2a-425c-a4f0-13886dd459d6", + "metadata": {}, + "source": [ + "The cells below may be considered via additional keyword arguments to the `explore` method. If these options are added, the following cells can be changed from \"raw\" to \"code\" and executed." + ] + }, + { + "cell_type": "markdown", + "id": "a1e212ce-466f-48db-9f3a-29bdf081f747", + "metadata": {}, + "source": [ + "### 3D customized choropleth" + ] + }, + { + "cell_type": "markdown", + "id": "a63025bc-882e-411e-a351-b5e3694c2914", + "metadata": {}, + "source": [ + "not actually intended to be attractive; no analogue in `explore`" + ] + }, + { + "cell_type": "raw", + "id": "faac05ec-4bc2-48d9-ad0b-dea626220455", + "metadata": {}, + "source": [ + "gdf.lb.explore(\n", + " column=\"HH_INC\",\n", + " cmap=\"plasma\",\n", + " scheme=\"fisher_jenks\",\n", + " k=8,\n", + " elevation=\"HH_INC\",\n", + " elevation_scale=0.1,\n", + " wireframe=True,\n", + " layer_kwargs={\n", + " \"get_line_color\": \"green\",\n", + " \"auto_highlight\": True,\n", + " },\n", + " map_kwargs={\n", + " \"view_state\": {\n", + " \"longitude\": -87.84,\n", + " \"latitude\": 43.13,\n", + " \"zoom\": 8.8,\n", + " \"pitch\": 44,\n", + " \"bearing\": -14,\n", + " },\n", + " },\n", + ")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python [conda env:lonboard]", + "language": "python", + "name": "conda-env-lonboard-py" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.8" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "state": {}, + "version_major": 2, + "version_minor": 0 + } + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/lonboard/geopandas.py b/lonboard/geopandas.py new file mode 100644 index 00000000..a577f9b5 --- /dev/null +++ b/lonboard/geopandas.py @@ -0,0 +1,318 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import geopandas as gpd +import numpy as np +import pandas as pd +from numpy import uint8 + +from lonboard import Map, viz +from lonboard.basemap import CartoStyle +from lonboard.colormap import apply_categorical_cmap, apply_continuous_cmap + +if TYPE_CHECKING: + from numpy.typing import ArrayLike, NDArray + + from lonboard.types.layer import ( + IntFloat, + PathLayerKwargs, + PolygonLayerKwargs, + ScatterplotLayerKwargs, + ) + from lonboard.types.map import MapKwargs + +__all__ = ["LonboardAccessor"] + +_QUERY_NAME_TRANSLATION = str.maketrans(dict.fromkeys("., -_/", "")) +_basemap_providers = { + "CartoDB Positron": CartoStyle.Positron, + "CartoDB Positron No Label": CartoStyle.PositronNoLabels, + "CartoDB Darkmatter": CartoStyle.DarkMatter, + "CartoDB Darkmatter No Label": CartoStyle.DarkMatterNoLabels, + "CartoDB Voyager": CartoStyle.Voyager, + "CartoDB Voyager No Label": CartoStyle.VoyagerNoLabels, +} +# Convert keys to lower case without spaces +_BASEMAP_PROVIDERS = { + k.translate(_QUERY_NAME_TRANSLATION).lower(): v + for k, v in _basemap_providers.items() +} + + +@pd.api.extensions.register_dataframe_accessor("lb") +class LonboardAccessor: + """Geopandas Extension class to provide the `explore` method.""" + + def __init__(self, pandas_obj: gpd.GeoDataFrame) -> None: + """Initialize geopandas extension.""" + self._validate(pandas_obj) + self._obj = pandas_obj + + @staticmethod + def _validate(obj: gpd.GeoDataFrame) -> None: + if not isinstance(obj, gpd.GeoDataFrame): + raise TypeError("Input Must be a geodataframe") + + def explore( # noqa: C901, PLR0912, PLR0913, PLR0915 + self, + column: str | None = None, + *, + cmap: str | None = None, + color: str | None = None, + m: Map | None = None, + tiles: str | None = None, + attr: str | list[str] | None = None, + tooltip: bool = False, + highlight: bool = False, + categorical: bool = False, + scheme: str | None = None, + k: int | None = 5, + vmin: float | None = None, + vmax: float | None = None, + height: int | str = 500, + classification_kwds: dict[str, str | IntFloat | ArrayLike | bool] | None = None, + layer_kwargs: ScatterplotLayerKwargs + | PathLayerKwargs + | PolygonLayerKwargs + | None = None, + map_kwargs: MapKwargs | None = None, + nan_color: list[int] | NDArray[np.uint8] | None = None, + alpha: float | None = 1, + ) -> Map: + """Explore a dataframe using lonboard and deckgl. + + Keyword Args: + column : Name of column on dataframe to visualize on map. + cmap : Name of matplotlib colormap to use. + color : single or array of colors passed to Layer.get_fill_color + or a lonboard.basemap object, or a string to a maplibre style basemap. + m: An existing Map object to plot onto. + tiles : Either a known string {"CartoDB Positron", + "CartoDB Positron No Label", "CartoDB Darkmatter", + "CartoDB Darkmatter No Label", "CartoDB Voyager", + "CartoDB Voyager No Label"} + attr : Map tile attribution; only required if passing custom tile URL. + tooltip : Whether to render a tooltip on hover on the map. + highlight : Whether to highlight each feature on mouseover (passed to + lonboard.Layer's auto_highlight). Defaults to False. + categorical : Whether the data should be treated as categorical or + continuous. + scheme : Name of a classification scheme defined by mapclassify.Classifier. + k : Number of classes to generate. Defaults to 5. + vmin : Minimum value for color mapping. + vmax : Maximum value for color mapping. + height: Height of the map in pixels, or valid CSS height property. + classification_kwds : Additional keyword arguments passed to + `mapclassify.classify`. + layer_kwargs : Additional keyword arguments passed to lonboard.viz layer + arguments (either `polygon_kwargs`, `scatterplot_kwargs`, or `path_kwargs`, + depending on input geometry type). + map_kwargs : Additional keyword arguments passed to lonboard.viz map_kwargs. + nan_color : Color used to shade NaN observations formatted as an RGBA list. + Defaults to [255, 255, 255, 255]. If no alpha channel is passed it is + assumed to be 255. + alpha : Alpha (opacity) parameter in the range (0,1) passed to + mapclassify.util.get_color_array. + + Returns: + lonboard.Map + a lonboard map with geodataframe included as a Layer object. + + """ + gdf = self._obj + + if map_kwargs is None: + map_kwargs = {} + if classification_kwds is None: + classification_kwds = {} + if layer_kwargs is None: + layer_kwargs = {} + if nan_color is None: + nan_color = [255, 255, 255, 255] + if not pd.api.types.is_list_like(nan_color): + raise ValueError("nan_color must be an iterable of 3 or 4 values") + if len(nan_color) != 4: + if len(nan_color) == 3: + nan_color = np.append(nan_color, [255]) + else: + raise ValueError("nan_color must be an iterable of 3 or 4 values") + + line = False # set color of lines, not fill_color + if ["LineString", "MultiLineString"] in gdf.geometry.geom_type.unique(): + line = True + if color: + if line: + layer_kwargs["get_color"] = color + else: + layer_kwargs["get_fill_color"] = color + if column is not None: + try: + from matplotlib import colormaps + except ImportError as e: + raise ImportError( + "you must have matplotlib installed to style by a column", + ) from e + + if column not in gdf.columns: + raise ValueError( + f"the designated column {column} is not in the dataframe", + ) + if gdf[column].dtype in ["O", "category"]: + categorical = True + if cmap is not None and cmap not in colormaps: + raise ValueError( + f"`cmap` must be one of {list(colormaps.keys())} but {cmap} was passed", + ) + if cmap is None: + cmap = "tab20" if categorical else "viridis" + if categorical: + color_array = _get_categorical_cmap(gdf[column], cmap, nan_color, alpha) + elif scheme is None: + if vmin is None: + vmin: int | float = np.nanmin(gdf[column]) + if vmax is None: + vmax: int | float = np.nanmax(gdf[column]) + # minmax scale the column first, matplotlib needs 0-1 + transformed = (gdf[column] - vmin) / (vmax - vmin) + color_array = apply_continuous_cmap( + values=transformed, + cmap=colormaps[cmap], + alpha=alpha, + ) + else: + try: + from mapclassify._classify_API import _classifiers + from mapclassify.util import get_color_array + + _klasses = list(_classifiers.keys()) + _klasses.append("userdefined") + except ImportError as e: + raise ImportError( + "You must have the package `mapclassify` >=2.7 installed to use the `scheme` keyword", + ) from e + if scheme.replace("_", "") not in _klasses: + raise ValueError( + f"The classification scheme must be a valid mapclassify classifier in {_klasses}, but {scheme} was passed instead", + ) + if k is not None and "k" in classification_kwds: + # k passed directly takes precedence + classification_kwds.pop("k") + color_array = get_color_array( + gdf[column], + scheme=scheme, + k=k, + cmap=cmap, + alpha=alpha, + nan_color=nan_color, + **classification_kwds, + ) + + if line: + layer_kwargs["get_color"] = color_array + + else: + layer_kwargs["get_fill_color"] = color_array + if tiles: + map_kwargs["basemap_style"] = _query_name(tiles) + if attr: + map_kwargs["custom_attribution"] = attr + map_kwargs["height"] = height + + layer_kwargs["auto_highlight"] = highlight + map_kwargs["show_tooltip"] = tooltip + + """ + additional loboard-specific arguments to consider + + # only polygons have z + if ["Polygon", "MultiPolygon"] in gdf.geometry.geom_type.unique(): + layer_kwargs["get_elevation"] = elevation + layer_kwargs["elevation_scale"] = elevation_scale + layer_kwargs["wireframe"] = wireframe + if isinstance(elevation, str): + if elevation in gdf.columns: + elevation: Series = gdf[elevation] + else: + raise ValueError( + f"the designated height column {elevation} is not in the dataframe", + ) + if not pd.api.types.is_numeric_dtype(elevation): + raise ValueError("elevation must be a numeric data type") + if elevation is not None: + layer_kwargs["extruded"] = True + """ + + new_m: Map = viz( + gdf, + polygon_kwargs=layer_kwargs, + scatterplot_kwargs=layer_kwargs, + path_kwargs=layer_kwargs, + map_kwargs=map_kwargs, + ) + if m is not None: + new_m = m.add_layer(new_m) + + return new_m + + +def _get_categorical_cmap( + categories: pd.Series, + cmap: str, + nan_color: str | NDArray[np.uint8] | NDArray[np.float64] | list[int], + alpha: float | None, +) -> NDArray[uint8]: + try: + from matplotlib import colormaps + except ImportError as e: + raise ImportError( + "this function requires the `matplotlib` package to be installed", + ) from e + + cat_codes = pd.Series(pd.Categorical(categories).codes, dtype="category") + # nans are encoded as -1 OR largest category depending on input type + # re-encode to always be last category + cat_codes = cat_codes.cat.rename_categories({-1: len(cat_codes.unique()) - 1}) + unique_cats = categories.dropna().unique() + n_cats = len(unique_cats) + colors = colormaps[cmap].resampled(n_cats)(list(range(n_cats)), alpha, bytes=True) + colors = np.vstack([colors, nan_color]) + temp_cmap = dict(zip(range(n_cats + 1), colors, strict=True)) + return apply_categorical_cmap(cat_codes, temp_cmap) + + +def _query_name(name: str) -> CartoStyle: + """Return basemap URL based on the name query (mimicking behavior from xyzservices). + + Returns a matching basemap from name contains the same letters in the same + order as the provider's name irrespective of the letter case, spaces, dashes + and other characters. See examples for details. + + Parameters + ---------- + name : str + Name of the tile provider. Formatting does not matter. + + Returns + ------- + match: lonboard.basemap + + Examples + -------- + >>> import xyzservices.providers as xyz + + All these queries return the same ``CartoDB.Positron`` TileProvider: + + >>> xyz._query_name("CartoDB Positron") + >>> xyz._query_name("cartodbpositron") + >>> xyz._query_name("cartodb-positron") + >>> xyz._query_name("carto db/positron") + >>> xyz._query_name("CARTO_DB_POSITRON") + >>> xyz._query_name("CartoDB.Positron") + + """ + name_clean = name.translate(_QUERY_NAME_TRANSLATION).lower() + if name_clean in _BASEMAP_PROVIDERS: + return _BASEMAP_PROVIDERS[name_clean] + + raise ValueError(f"No matching provider found for the query '{name}'.")