diff --git a/examples/explore.ipynb b/examples/explore.ipynb
new file mode 100644
index 00000000..7b9c92a2
--- /dev/null
+++ b/examples/explore.ipynb
@@ -0,0 +1,3721 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "a76948e2-eda2-4c6f-8fe2-341b67ac5531",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "%load_ext autoreload\n",
+ "%autoreload 2\n",
+ "\n",
+ "from warnings import warn\n",
+ "\n",
+ "import geodatasets\n",
+ "import geopandas as gpd\n",
+ "import lonboard.geopandas\n",
+ "import numpy as np\n",
+ "from lonboard import PolygonLayer\n",
+ "from mapclassify import classify"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "id": "1e09eb28-7b9a-4dd6-a11a-f9d26bd5e793",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Author: eli knaap\n",
+ "\n",
+ "mapclassify: 2.8.1\n",
+ "lonboard : 0.10.3\n",
+ "geopandas : 1.0.1\n",
+ "geodatasets: 2024.8.0\n",
+ "numpy : 2.0.2\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%load_ext watermark\n",
+ "%watermark -a 'eli knaap' -iv"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "3902a38a-84c2-492a-b8b8-fad2b4afef72",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "gdf = gpd.read_file(geodatasets.get_path(\"geoda.milwaukee1\"))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "id": "9b0db679-e16b-4111-b24f-19de0c544fc5",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "gdf = gdf.to_crs(gdf.estimate_utm_crs())"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "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": 6,
+ "id": "11c477e4-b3c0-489c-a85c-2a9ac9e48d8c",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "
Make this Notebook Trusted to load map: File -> Trust Notebook
"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 6,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "gdf.explore()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "id": "22485cad-e47b-409a-accf-0da27419db7b",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "/Users/knaaptime/Dropbox/projects/lonboard/lonboard/_geoarrow/ops/reproject.py:115: UserWarning: Input being reprojected to EPSG:4326 CRS.\n",
+ "Lonboard is only able to render data in EPSG:4326 projection.\n",
+ " warnings.warn(\n"
+ ]
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "b82e0b073c10424a8d206bab1ef30138",
+ "version_major": 2,
+ "version_minor": 1
+ },
+ "text/plain": [
+ "Map(basemap_style=, cu…"
+ ]
+ },
+ "execution_count": 7,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "from lonboard import basemap\n",
+ "\n",
+ "gdf.lb.explore(\n",
+ " tiles=\"CartoDB Positron\",\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "93da152a-6409-4764-ab24-f773e3d09fb9",
+ "metadata": {},
+ "source": [
+ "## boorish (unclassed :P) choropleth"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "id": "487cafd3-4735-4c2c-b19f-6ff6dcdfe93c",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "Make this Notebook Trusted to load map: File -> Trust Notebook
"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 8,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "gdf.explore(\"HH_INC\", tiles=\"CartoDB Darkmatter\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "id": "f044e655-32be-4ce1-b015-223ed9c71de1",
+ "metadata": {
+ "scrolled": true
+ },
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "/Users/knaaptime/Dropbox/projects/lonboard/lonboard/_geoarrow/ops/reproject.py:115: UserWarning: Input being reprojected to EPSG:4326 CRS.\n",
+ "Lonboard is only able to render data in EPSG:4326 projection.\n",
+ " warnings.warn(\n"
+ ]
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "715b167d7ba24bf2b4c58f7e5c395164",
+ "version_major": 2,
+ "version_minor": 1
+ },
+ "text/plain": [
+ "Map(basemap_style=Make this Notebook Trusted to load map: File -> Trust Notebook
"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 11,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "gdf.explore(\n",
+ " \"HH_INC\",\n",
+ " scheme=\"quantiles\",\n",
+ " k=6,\n",
+ " cmap=\"YlOrBr\",\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "id": "e70b95c3-09b2-40be-8f5e-fd1482388be2",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "/Users/knaaptime/Dropbox/projects/lonboard/lonboard/_geoarrow/ops/reproject.py:115: UserWarning: Input being reprojected to EPSG:4326 CRS.\n",
+ "Lonboard is only able to render data in EPSG:4326 projection.\n",
+ " warnings.warn(\n"
+ ]
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "aa1ffb245bff469a9d2d7b89174050b9",
+ "version_major": 2,
+ "version_minor": 1
+ },
+ "text/plain": [
+ "Map(basemap_style=Make this Notebook Trusted to load map: File -> Trust Notebook
"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 14,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "classify_kwds = {\"bins\": [20000, 40000, 800000, 2000000]}\n",
+ "\n",
+ "gdf.explore(\n",
+ " \"HH_INC\",\n",
+ " cmap=\"YlOrBr\",\n",
+ " scheme=\"user_defined\",\n",
+ " classification_kwds=classify_kwds,\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 15,
+ "id": "ad5c4e9f-7dcc-4144-b5ba-77987cea0def",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "/Users/knaaptime/Dropbox/projects/lonboard/lonboard/_geoarrow/ops/reproject.py:115: UserWarning: Input being reprojected to EPSG:4326 CRS.\n",
+ "Lonboard is only able to render data in EPSG:4326 projection.\n",
+ " warnings.warn(\n"
+ ]
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "7dcbea826d014ef49cdf171f8129baf0",
+ "version_major": 2,
+ "version_minor": 1
+ },
+ "text/plain": [
+ "Map(basemap_style=Make this Notebook Trusted to load map: File -> Trust Notebook
"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 18,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "m = linedf.explore(\n",
+ " \"len\", cmap=\"viridis_r\", scheme=\"quantiles\", k=10, tiles=\"CartoDB Darkmatter\"\n",
+ ")\n",
+ "gdf.set_geometry(gdf.centroid).explore(color=\"magenta\", m=m)\n",
+ "m"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 19,
+ "id": "54df1700-2971-4813-9842-aa0116b46e20",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "/Users/knaaptime/Dropbox/projects/lonboard/lonboard/_geoarrow/ops/reproject.py:115: UserWarning: Input being reprojected to EPSG:4326 CRS.\n",
+ "Lonboard is only able to render data in EPSG:4326 projection.\n",
+ " warnings.warn(\n",
+ "/Users/knaaptime/Dropbox/projects/lonboard/lonboard/_geoarrow/ops/reproject.py:115: UserWarning: Input being reprojected to EPSG:4326 CRS.\n",
+ "Lonboard is only able to render data in EPSG:4326 projection.\n",
+ " warnings.warn(\n"
+ ]
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "c1b361ce82bb46b7b820ad023e0192a7",
+ "version_major": 2,
+ "version_minor": 1
+ },
+ "text/plain": [
+ "Map(basemap_style=Make this Notebook Trusted to load map: File -> Trust Notebook
"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 21,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "gdf.explore(\n",
+ " \"i\",\n",
+ " categorical=True,\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 22,
+ "id": "a7f86337-b4ed-4ac6-929c-13d053f21767",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "/Users/knaaptime/Dropbox/projects/lonboard/lonboard/_geoarrow/ops/reproject.py:115: UserWarning: Input being reprojected to EPSG:4326 CRS.\n",
+ "Lonboard is only able to render data in EPSG:4326 projection.\n",
+ " warnings.warn(\n"
+ ]
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "84e99ffd7cd042f99b6eb060f3ba960b",
+ "version_major": 2,
+ "version_minor": 1
+ },
+ "text/plain": [
+ "Map(basemap_style=Make this Notebook Trusted to load map: File -> Trust Notebook
"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 26,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "gdf.explore(\"q5\", categorical=True, cmap=\"tab20b\", tiles=\"CartoDB Positron\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 27,
+ "id": "9bd34a9c-6a45-4f53-a9ca-19324e6aa32d",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "/Users/knaaptime/Dropbox/projects/lonboard/lonboard/_geoarrow/ops/reproject.py:115: UserWarning: Input being reprojected to EPSG:4326 CRS.\n",
+ "Lonboard is only able to render data in EPSG:4326 projection.\n",
+ " warnings.warn(\n"
+ ]
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "ec59bf072c244f018f73c1a412af0232",
+ "version_major": 2,
+ "version_minor": 1
+ },
+ "text/plain": [
+ "Map(basemap_style=, cu…"
+ ]
+ },
+ "execution_count": 27,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "m = gdf.lb.explore(\n",
+ " \"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": "4e926525-d0a8-40dd-a195-89a774bbe8a4",
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 28,
+ "id": "c54f7abe-ca10-467a-baf0-ff6c499ea4e8",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "/Users/knaaptime/Dropbox/projects/lonboard/lonboard/_geoarrow/ops/reproject.py:115: UserWarning: Input being reprojected to EPSG:4326 CRS.\n",
+ "Lonboard is only able to render data in EPSG:4326 projection.\n",
+ " warnings.warn(\n"
+ ]
+ }
+ ],
+ "source": [
+ "m = gdf.lb.explore(\"q5\", categorical=True, cmap=\"RdBu\", alpha=0.5)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 29,
+ "id": "433064c8-6f54-43fe-9e73-1754100a6d50",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "be7189af7be64473a06345cd06fc62d0",
+ "version_major": 2,
+ "version_minor": 1
+ },
+ "text/plain": [
+ "Map(basemap_style= widget:\n",
+ " layer.get_fill_color = get_color_array(vals, classifier, k=k, cmap=cmap)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 36,
+ "id": "cb646dce-d4ef-48d4-8016-f8a9ed9fb97e",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from matplotlib import colormaps"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 37,
+ "id": "0577a3a1-74e2-4d89-b1ce-e4ddd27340c4",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "/Users/knaaptime/Dropbox/projects/lonboard/lonboard/_geoarrow/ops/reproject.py:115: UserWarning: Input being reprojected to EPSG:4326 CRS.\n",
+ "Lonboard is only able to render data in EPSG:4326 projection.\n",
+ " warnings.warn(\n"
+ ]
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "089e2b4b9de84b84a3db28f87cc93126",
+ "version_major": 2,
+ "version_minor": 1
+ },
+ "text/plain": [
+ "Map(basemap_style= >"
+ ]
+ },
+ "execution_count": 39,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "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": 40,
+ "id": "73c7a823-c3fd-4ade-aa73-5709b0ee29d7",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "/Users/knaaptime/Dropbox/projects/lonboard/lonboard/_geoarrow/ops/reproject.py:115: UserWarning: Input being reprojected to EPSG:4326 CRS.\n",
+ "Lonboard is only able to render data in EPSG:4326 projection.\n",
+ " warnings.warn(\n"
+ ]
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "d71e678ca73746efafc716406fede27e",
+ "version_major": 2,
+ "version_minor": 1
+ },
+ "text/plain": [
+ "Map(basemap_style= None: # noqa: ANN001, D107
+ self._validate(pandas_obj)
+ self._obj = pandas_obj
+
+ @staticmethod
+ def _validate(obj) -> None: # noqa: ANN001
+ if not isinstance(obj, gpd.GeoDataFrame):
+ raise TypeError("must be a geodataframe")
+
+ def explore( # noqa: PLR0913
+ self,
+ column: str | None = None,
+ cmap: str | None = None,
+ scheme: str | None = None,
+ k: int | None = 6,
+ categorical: bool = False, # noqa: FBT001, FBT002
+ elevation: str | np.ndarray = None,
+ elevation_scale: float | None = 1,
+ alpha: float | None = 1,
+ layer_kwargs: dict[str, Any] | None = None,
+ map_kwargs: dict[str, Any] | None = None,
+ classification_kwds: dict[str, Any] | None = None,
+ nan_color: list[int] | np.ndarray[int] | None = None,
+ color: str | None = None,
+ vmin: float | None = None,
+ vmax: float | None = None,
+ wireframe: bool = False, # noqa: FBT001, FBT002
+ tiles: str | None = None,
+ highlight: bool = False, # noqa: FBT001, FBT002
+ m: Map | None = None,
+ ) -> 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.
+ scheme : Name of a classification scheme defined by mapclassify.Classifier.
+ k : Number of classes to generate. Defaults to 6.
+ categorical : Whether the data should be treated as categorical or
+ continuous.
+ elevation : Name of column on the dataframe used to extrude each geometry or
+ an array-like in the same order as observations. Defaults to None.
+ elevation_scale : Constant scaler multiplied by elevation value.
+ alpha : Alpha (opacity) parameter in the range (0,1) passed to
+ mapclassify.util.get_color_array.
+ 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.
+ classification_kwds : Additional keyword arguments passed to
+ `mapclassify.classify`.
+ 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.
+ 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.
+ vmin : Minimum value for color mapping.
+ vmax : Maximum value for color mapping.
+ wireframe : Whether to use wireframe styling in deckgl.
+ tiles : Either a known string {"CartoDB Positron",
+ "CartoDB Positron No Label", "CartoDB Darkmatter",
+ "CartoDB Darkmatter No Label", "CartoDB Voyager",
+ "CartoDB Voyager No Label"}
+ highlight : Whether to highlight each feature on mouseover (passed to
+ lonboard.Layer's auto_highlight). Defaults to False.
+ m: An existing Map object to plot onto.
+
+ Returns:
+ lonboard.Map
+ a lonboard map with geodataframe included as a Layer object.
+
+ """
+ return _dexplore(
+ self._obj,
+ column=column,
+ cmap=cmap,
+ scheme=scheme,
+ k=k,
+ categorical=categorical,
+ elevation=elevation,
+ elevation_scale=elevation_scale,
+ alpha=alpha,
+ layer_kwargs=layer_kwargs,
+ map_kwargs=map_kwargs,
+ classification_kwds=classification_kwds,
+ nan_color=nan_color,
+ color=color,
+ vmin=vmin,
+ vmax=vmax,
+ wireframe=wireframe,
+ tiles=tiles,
+ highlight=highlight,
+ m=m,
+ )
+
+
+def _dexplore( # noqa: C901, PLR0912, PLR0913, PLR0915
+ gdf, # noqa: ANN001
+ *,
+ column, # noqa: ANN001
+ cmap, # noqa: ANN001
+ scheme, # noqa: ANN001
+ k, # noqa: ANN001
+ categorical, # noqa: ANN001
+ elevation, # noqa: ANN001
+ elevation_scale, # noqa: ANN001
+ alpha, # noqa: ANN001
+ layer_kwargs, # noqa: ANN001
+ map_kwargs, # noqa: ANN001
+ classification_kwds, # noqa: ANN001
+ nan_color, # noqa: ANN001
+ color, # noqa: ANN001
+ vmin, # noqa: ANN001
+ vmax, # noqa: ANN001
+ wireframe, # noqa: ANN001
+ tiles, # noqa: ANN001
+ highlight, # noqa: ANN001
+ m, # noqa: ANN001
+) -> Map:
+ """Explore a dataframe using lonboard and deckgl.
+
+ See the public docstring for detailed parameter information
+
+ Returns
+ -------
+ lonboard.Map
+ a lonboard map with geodataframe included as a Layer object.
+
+ """
+ if map_kwargs is None:
+ map_kwargs = {}
+ if classification_kwds is None:
+ classification_kwds = {}
+ if layer_kwargs is None:
+ layer_kwargs = {}
+ if isinstance(elevation, str):
+ if elevation in gdf.columns:
+ elevation = 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
+ 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")
+
+ # 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
+ layer_kwargs["auto_highlight"] = highlight
+
+ 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 = np.nanmin(gdf[column])
+ if vmax is None:
+ vmax = 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 `mapclassify` package installed to use the "
+ "`scheme` keyword",
+ ) from e
+ if scheme.replace("_", "") not in _klasses:
+ raise ValueError(
+ "the classification scheme must be a valid mapclassify"
+ f"classifier in {_klasses},"
+ f"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)
+ new_m = 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, cmap, nan_color, alpha): # noqa: ANN001, ANN202
+ try:
+ from matplotlib import colormaps
+ except ImportError as e:
+ raise ImportError(
+ "this function requires the `lonboard` 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))
+ return apply_categorical_cmap(cat_codes, temp_cmap)
+
+def _query_name(name: str) -> basemap:
+ """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")
+
+ """
+ providers = {
+ "CartoDB Positron": basemap.CartoBasemap.Positron,
+ "CartoDB Positron No Label": basemap.CartoBasemap.PositronNoLabels,
+ "CartoDB Darkmatter": basemap.CartoBasemap.DarkMatter,
+ "CartoDB Darkmatter No Label": basemap.CartoBasemap.DarkMatterNoLabels,
+ "CartoDB Voyager": basemap.CartoBasemap.Voyager,
+ "CartoDB Voyager No Label": basemap.CartoBasemap.VoyagerNoLabels,
+ }
+ xyz_flat_lower = {
+ k.translate(QUERY_NAME_TRANSLATION).lower(): v
+ for k, v in providers.items()
+ }
+ name_clean = name.translate(QUERY_NAME_TRANSLATION).lower()
+ if name_clean in xyz_flat_lower:
+ return xyz_flat_lower[name_clean]
+
+ raise ValueError(f"No matching provider found for the query '{name}'.")
diff --git a/pyproject.toml b/pyproject.toml
index b58adda8..f43334b1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -78,6 +78,7 @@ dev = [
"geoarrow-rust-core>=0.3.0",
"geodatasets>=2024.8.0",
"jupyterlab>=4.3.3",
+ "mapclassify>=2.8.1",
"matplotlib>=3.7.5",
"movingpandas>=0.20.0",
"palettable>=3.3.3",